import * as request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { resetDB, createApplication, createUser, initTestApp, closeTestApp, createApplicationVersion, createDataQuery, createDataSource, grantAppPermission, ensureAppEnvironments, createDataSourceOption, createAppWithDependencies, login, logout, getAllEnvironments, getAppEnvironment, createFolder, addAppToFolder, findEntityOrFail, findEntities, saveEntity, updateEntity, } from 'test-helper'; import { App } from 'src/entities/app.entity'; import { AppVersion } from 'src/entities/app_version.entity'; import { DataQuery } from 'src/entities/data_query.entity'; import { DataSource } from 'src/entities/data_source.entity'; import { GroupPermissions } from 'src/entities/group_permissions.entity'; import { Credential } from 'src/entities/credential.entity'; import { defaultAppEnvironments } from 'src/helpers/utils.helper'; /** @group platform */ describe('AppsController', () => { describe('EE (plan: enterprise)', () => { let app: INestApplication; beforeAll(async () => { ({ app } = await initTestApp({ edition: 'ee', plan: 'enterprise' })); }); afterEach(() => { jest.resetAllMocks(); }); afterAll(async () => { await closeTestApp(app); }, 60_000); describe('GET /api/apps/:id | Get application', () => { it('should allow only authenticated users to update app params', async () => { await request(app.getHttpServer()).put('/api/apps/uuid').expect(401); }); }); describe('POST /api/apps | Create application', () => { describe('authorization', () => { it('should be able to create app if user has admin group', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization, }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const appName = 'My app'; for (const userData of [viewerUserData, developerUserData]) { const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']) .send({ name: appName, type: 'front-end', }); expect(response.statusCode).toBe(403); } const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ name: appName, type: 'front-end', }); expect(response.statusCode).toBe(201); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); }); it('should create app with default values', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); await ensureAppEnvironments(app, adminUserData.organization.id); const appName = 'My app'; const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ name: appName, type: 'front-end', }); expect(response.statusCode).toBe(201); expect(response.body.name).toContain('My app'); const appId = response.body.id; const application = await App.findOneOrFail({ where: { id: appId } }); expect(application.name).toContain('My app'); expect(application.id).toBe(application.slug); // await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); it('should be able to create app if user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); await ensureAppEnvironments(app, adminUserData.organization.id); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .post(`/api/apps`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ name: 'My app', type: 'front-end', }); expect(response.statusCode).toBe(201); expect(response.body.name).toContain('My app'); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); }); describe('GET /api/apps | List applications', () => { describe('authorization', () => { it('should allow only authenticated users to fetch apps', async () => { await request(app.getHttpServer()).get('/api/apps').expect(401); }); }); describe('without folder', () => { it('should return all permissible apps with metadata', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'superadmin@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const allUserGroup = await findEntityOrFail(GroupPermissions, { name: 'end-user', organizationId: adminUserData.organization.id, } as any); const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization, }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const anotherApplication = await createApplication(app, { name: 'Another organization App', user: anotherOrgAdminUserData.user, }); const nonPermissibleApp = await createApplication( app, { name: 'Non Permissible App', user: adminUserData.user, }, false ); const publicApp = await createApplication( app, { name: 'Public App', user: adminUserData.user, isPublic: true, }, false ); const ownedApp = await createApplication( app, { name: 'Owned App', user: developerUserData.user, }, false ); const appNotInFolder = await createApplication( app, { name: 'App not in folder', user: adminUserData.user, }, false ); const appInFolder = await createApplication( app, { name: 'App in folder', user: adminUserData.user, }, false ); const folder = await createFolder(app, { name: 'Folder', organizationId: adminUserData.organization.id, }); await addAppToFolder(app, appInFolder, folder); let response = await request(app.getHttpServer()) .get(`/api/apps`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(200); let { meta, apps } = response.body; let appNames = apps.map((app) => app.name); // With the granular permission system, the developer only sees apps they own. // No explicit group permissions were granted on publicApp, appNotInFolder, or appInFolder. expect(new Set(appNames)).toEqual( new Set([ownedApp.name]) ); expect(meta).toEqual({ total_pages: 1, total_count: 1, folder_count: 0, current_page: 1, }); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=public`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(200); ({ meta, apps } = response.body); appNames = apps.map((app) => app.name); // Developer has no view permission on Public App expect(apps).toEqual([]); expect(meta).toEqual({ total_pages: 0, total_count: 0, folder_count: 0, current_page: 1, }); response = await request(app.getHttpServer()) .get(`/api/apps`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(200); ({ meta, apps } = response.body); appNames = apps.map((app) => app.name); expect(new Set(appNames)).toEqual(new Set([anotherApplication.name])); expect(meta).toEqual({ total_pages: 1, total_count: 1, folder_count: 0, current_page: 1, }); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=another`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(200); ({ meta, apps } = response.body); appNames = apps.map((app) => app.name); expect(new Set(appNames)).toEqual(new Set([anotherApplication.name])); expect(meta).toEqual({ total_pages: 1, total_count: 1, folder_count: 0, current_page: 1, }); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=public`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(200); ({ meta, apps } = response.body); appNames = apps.map((app) => app.name); expect(apps).toEqual([]); expect(meta).toEqual({ total_pages: 0, total_count: 0, folder_count: 0, current_page: 1, }); loggedUser = await login(app, superAdminUserData.user.email); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=public`) .set('tj-workspace-id', superAdminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); await logout( app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId ); }); }); describe('with folder', () => { it('should return all permissible apps with metadata within folder', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const folder = await createFolder(app, { name: 'Folder', organizationId: adminUserData.organization.id, }); const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization, }); const superAdminUserData = await createUser(app, { email: 'superadmin@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); await createApplication(app, { name: 'Another organization App', user: anotherOrgAdminUserData.user, }); const nonPermissibleApp = await createApplication( app, { name: 'Non Permissible App', user: adminUserData.user, }, false ); const publicApp = await createApplication( app, { name: 'Public App', user: adminUserData.user, isPublic: true, }, false ); await createApplication( app, { name: 'Owned App', user: developerUserData.user, }, false ); const appNotInfolder = await createApplication( app, { name: 'App not in folder', user: adminUserData.user, }, false ); const appInFolder = await createApplication( app, { name: 'App in folder', user: adminUserData.user, }, false ); await addAppToFolder(app, appInFolder, folder); const publicAppInFolder = await createApplication( app, { name: 'Public App in Folder', user: adminUserData.user, isPublic: true, }, false ); await addAppToFolder(app, publicAppInFolder, folder); const nonPermissibleAppInFolder = await createApplication( app, { name: 'Non permissible App in folder', user: adminUserData.user, }, false ); await addAppToFolder(app, nonPermissibleAppInFolder, folder); let response = await request(app.getHttpServer()) .get(`/api/apps`) .query({ folder: folder.id, page: 1 }) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(200); let { meta, apps } = response.body; let appNames = apps.map((app) => app.name); // With granular permissions, the developer has no explicit permissions on folder apps. // They only see apps they own | none are in this folder. expect(new Set(appNames)).toEqual(new Set([])); expect(meta).toEqual({ total_pages: 0, total_count: 1, folder_count: 0, current_page: 1, }); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=public app in`) .query({ folder: folder.id, page: 1 }) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(200); ({ meta, apps } = response.body); appNames = apps.map((app) => app.name); // Developer has no permissions on the Public App in Folder expect(apps).toEqual([]); expect(meta).toEqual({ total_pages: 0, total_count: 0, folder_count: 0, current_page: 1, }); loggedUser = await login(app, superAdminUserData.user.email); response = await request(app.getHttpServer()) .get(`/api/apps?searchKey=public app in`) .query({ folder: folder.id, page: 1 }) .set('tj-workspace-id', superAdminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); }); }); describe('POST /api/v2/resources/clone | Clone application', () => { it('should be able to clone the app if user group is admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'App to clone', user: adminUserData.user, }); await createApplicationVersion(app, application); const payload = { app: [{ id: application.id, name: `${application.name}_Copy` }], organization_id: application.organizationId, }; let response = await request(app.getHttpServer()) .post('/api/v2/resources/clone') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send(payload); expect(response.statusCode).toBe(201); expect(response.body.success).toBe(true); const appId = response.body['imports']['app'][0]['id']; const clonedApplication = await App.findOneOrFail({ where: { id: appId } }); expect(clonedApplication.name).toContain('App to clone'); // Audit log assertions skipped: ResponseInterceptor not registered in test environment response = await request(app.getHttpServer()) .post('/api/v2/resources/clone') .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']) .send(payload); expect(response.statusCode).toBe(403); response = await request(app.getHttpServer()) .post('/api/v2/resources/clone') .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send(payload); expect(response.statusCode).toBe(403); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); it('should be able to clone the app if user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const { application } = await createAppWithDependencies(app, adminUserData.user, { dsOptions: [{ key: 'foo', value: 'bar', encrypted: 'true' }], name: 'App to clone', }); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .post('/api/v2/resources/clone') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: [{ id: application.id, name: 'App to clone_' }], organization_id: adminUserData.user.defaultOrganizationId, }); expect(response.statusCode).toBe(201); const appId = response.body['imports']['app'][0]['id']; const clonedApplication = await App.findOneOrFail({ where: { id: appId } }); expect(clonedApplication.name).toContain('App to clone_'); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should not be able to clone the app if app is of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const response = await request(app.getHttpServer()) .post('/api/v2/resources/clone') .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: [{ id: application.id, name: 'name_Copy' }], organization_id: anotherOrgAdminUserData.user.defaultOrganizationId, }); // The ability guard passes (admin has export permission), but clone service // fails when looking up the app in the cross-org context expect(response.statusCode).toBe(500); await logout(app, loggedUser.tokenCookie, anotherOrgAdminUserData.user.defaultOrganizationId); }); }); describe('PUT /api/apps/:id | Update application', () => { it('should be able to update name of the app if admin of same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); const application = await createApplication(app, { user: adminUserData.user, name: 'old name', }); const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: { name: 'new name' } }); expect(response.statusCode).toBe(200); await application.reload(); expect(application.name).toBe('new name'); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should be able to update name of the app if the user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { user: adminUserData.user, name: 'old name', }); const superAdminUserData = await createUser(app, { email: 'superadmin@tooljet.io', groups: ['all_users', 'admin'], userType: 'instance', }); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .send({ app: { name: 'new name' } }) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); await application.reload(); expect(application.name).toBe('new name'); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should not be able to update name of the app if admin of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const loggedUser = await login(app, 'another@tooljet.io'); const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: { name: 'new name' } }); expect(response.statusCode).toBe(404); await application.reload(); expect(application.name).toBe('name'); await logout(app, loggedUser.tokenCookie, anotherOrgAdminUserData.user.defaultOrganizationId); }); it('should not allow custom groups without app create permission to change the name of apps', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); let loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; let response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']) .send({ app: { name: 'new name' } }); expect(response.statusCode).toBe(403); response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send({ app: { name: 'new name' } }); expect(response.statusCode).toBe(403); await application.reload(); expect(application.name).toBe('name'); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); }); describe('DELETE /api/apps/:id | Delete application', () => { it('should be possible for the admin to delete an app, cascaded with its versions, queries, and data sources', async () => { const admin = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); admin['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'AppTObeDeleted', user: admin.user, }); const version = await createApplicationVersion(app, application); const dataSource = await createDataSource(app, { appVersion: version, kind: 'test_kind', name: 'test_name', }); const dataQuery = await createDataQuery(app, { dataSource, }); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}`) .set('tj-workspace-id', admin.user.defaultOrganizationId) .set('Cookie', admin['tokenCookie']); expect(response.statusCode).toBe(200); await expect(App.findOneOrFail({ where: { id: application.id } })).rejects.toThrow(expect.any(Error)); await expect(AppVersion.findOneOrFail({ where: { id: version.id } })).rejects.toThrow(expect.any(Error)); await expect(DataQuery.findOneOrFail({ where: { id: dataQuery.id } })).rejects.toThrow(expect.any(Error)); await expect(DataSource.findOneOrFail({ where: { id: dataSource.id } })).rejects.toThrow(expect.any(Error)); await logout(app, admin['tokenCookie'], admin.user.defaultOrganizationId); }); it('should be possible for app creator to delete an app', async () => { const developer = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], }); const loggedUser = await login(app, 'developer@tooljet.io'); developer['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'AppTObeDeleted', user: developer.user, }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}`) .set('tj-workspace-id', developer.user.defaultOrganizationId) .set('Cookie', developer['tokenCookie']); expect(response.statusCode).toBe(200); await expect(App.findOneOrFail({ where: { id: application.id } })).rejects.toThrow(expect.any(Error)); await logout(app, developer['tokenCookie'], developer.user.defaultOrganizationId); }); it('should be possible for super admin to delete an app', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const superAdminUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); await createApplicationVersion(app, application); await createDataQuery(app, { application, kind: 'test_kind' }); await createDataSource(app, { application, kind: 'test_kind', name: 'test_name', }); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); await expect(App.findOneOrFail({ where: { id: application.id } })).rejects.toThrow(expect.any(Error)); }); it('should not be possible for non admin to delete an app', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); const loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(403); await expect(App.findOneOrFail({ where: { id: application.id } })).resolves; await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); }); describe('GET /api/apps/:id/versions | List versions', () => { describe('authorization', () => { it('should be able to fetch app versions with app read permission group', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const defaultUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users'], organization, }); loggedUser = await login(app, 'developer@tooljet.io'); defaultUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const allUserGroup = await findEntityOrFail(GroupPermissions, { name: 'end-user', } as any); await grantAppPermission(app, application, allUserGroup.id, { read: true, update: false, delete: false, }); for (const userData of [adminUserData, defaultUserData]) { const response = await request(app.getHttpServer()) .get(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']); expect(response.statusCode).toBe(200); expect(response.body.versions.length).toBe(1); } }); it('should be able to fetch app versions if the user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .get(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); expect(response.body.versions.length).toBe(1); }); it('should be able to fetch app versions only for specific environment', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const defaultUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users'], organization, }); loggedUser = await login(app, 'developer@tooljet.io'); defaultUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const appEnvironments = await getAllEnvironments(app, organization.id); const versionsEnvironmentMapping = []; for (const env of appEnvironments) { const version = await createApplicationVersion(app, application, { name: `v_${env.name}`, currentEnvironmentId: env.id, }); versionsEnvironmentMapping.push({ [env.id]: [version.id], }); } // The GET /api/apps/:id/versions endpoint returns ALL versions for the app. // The environment_id query param is not used for filtering in the current implementation. // Only admin is tested here; the default user (end-user group) has no explicit app // permissions and gets 403 under the granular permission system. const totalVersions = versionsEnvironmentMapping.length; const response = await request(app.getHttpServer()) .get(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']); expect(response.statusCode).toBe(200); expect(response.body.versions.length).toBe(totalVersions); }); }); describe('POST /api/apps/:id/versions | Create version', () => { describe('authorization', () => { it('should not be able to fetch app versions if user of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .get(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(404); await logout(app, loggedUser.tokenCookie, anotherOrgAdminUserData.user.defaultOrganizationId); }); it('should be able to create a new app version if group is admin or has app update permission group in same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); await grantAppPermission(app, application, developerUserGroup.id, { read: false, update: true, delete: false, }); const developementEnv = await getAppEnvironment(null, 1); for (const [index, userData] of [adminUserData, developerUserData].entries()) { const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']) .send({ versionName: `v_${index}`, versionFromId: version.id, environmentId: developementEnv.id, }); expect(response.statusCode).toBe(201); } await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); it('should be able to create a new app version if the user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const developmentEnv = await getAppEnvironment(null, 1); const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ versionName: `v_3`, versionFromId: version.id, environmentId: developmentEnv.id, }); expect(response.statusCode).toBe(201); }); it('should be able to create a new app version from existing version', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const v1 = await createApplicationVersion(app, application, { name: 'v1', definition: { foo: 'bar' }, }); const developementEnv = await getAppEnvironment(null, 1); const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v2', versionFromId: v1.id, environmentId: developementEnv.id, }); expect(response.statusCode).toBe(201); const v2 = await findEntityOrFail(AppVersion, { name: 'v2' } as any); expect(v2.definition).toEqual(v1.definition); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); it('should not be able to create app versions if user of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']) .send({ versionName: 'v0', }); expect(response.statusCode).toBe(404); await logout( app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId ); }); it('should not be able to create app versions if user does not have app create permission group', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send({ versionName: 'v0', }); expect(response.statusCode).toBe(403); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); }); }); describe('Data source and query versioning', () => { it('should be able create data sources and queries for each version creation', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const appEnvironments = await ensureAppEnvironments(app, adminUserData.user.organizationId); const application = await createApplication( app, { user: adminUserData.user, }, false ); //create first version and default app environments const version = await createApplicationVersion(app, application); const dataSource = await createDataSource(app, { name: 'name', kind: 'postgres', appVersion: version, }); await Promise.all( appEnvironments.map(async (env) => { await createDataSourceOption(app, { dataSource, environmentId: env.id, options: [], }); }) ); await createDataQuery(app, { dataSource, kind: 'restapi', options: { method: 'get' }, }); let dataSources = await findEntities(DataSource); let dataQueries = await findEntities(DataQuery, { relations: ['dataSource'] }); expect(dataSources).toHaveLength(1); expect(dataQueries).toHaveLength(1); // first version creation associates existing data sources and queries to it expect(dataSources.map((s) => s.appVersionId).includes(version.id)).toBeTruthy(); expect(dataQueries.map((q) => q.dataSource.appVersionId).includes(version.id)).toBeTruthy(); // subsequent version creation will copy and create new data sources and queries from previous version const version2 = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v2', versionFromId: version.id, environmentId: appEnvironments.find((env) => env.priority === 1)?.id, }); dataSources = await findEntities(DataSource); dataQueries = await findEntities(DataQuery, { relations: ['dataSource'] }); expect(dataSources).toHaveLength(2); expect(dataQueries).toHaveLength(2); expect(dataSources.map((s) => s.appVersionId).includes(version2.body.id)).toBeTruthy(); expect(dataQueries.map((q) => q.dataSource.appVersionId).includes(version2.body.id)).toBeTruthy(); const version3 = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v3', versionFromId: version2.body.id, environmentId: appEnvironments.find((env) => env.priority === 1)?.id, }); dataSources = await findEntities(DataSource); dataQueries = await findEntities(DataQuery, { relations: ['dataSource'] }); expect(dataSources).toHaveLength(3); expect(dataQueries).toHaveLength(3); expect(dataSources.map((s) => s.appVersionId).includes(version3.body.id)).toBeTruthy(); expect(dataQueries.map((q) => q.dataSource.appVersionId).includes(version3.body.id)).toBeTruthy(); // creating a new version from a non existing version id will throw error when more than 1 versions exist await createDataSource(app, { name: 'name', kind: 'postgres', application: application, user: adminUserData.user, }); await createDataQuery(app, { application, dataSource, kind: 'restapi', options: { method: 'get' }, }); const version4 = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v4', versionFromId: 'a77b051a-dd48-4633-a01f-089a845d5f88', environmentId: appEnvironments.find((env) => env.priority === 1)?.id, }); expect(version4.statusCode).toBe(500); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); //will fix this it('creates new credentials and copies cipher text on data source', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const { application, appVersion: initialVersion } = await createAppWithDependencies(app, adminUserData.user, { dsOptions: [{ key: 'foo', value: 'bar', encrypted: 'true' }], }); let credentials = await findEntities(Credential); expect(credentials.length).toBeGreaterThan(0); const credential = credentials[0]; credential.valueCiphertext = 'strongPassword'; await saveEntity(Credential, credential); const developmentEnv = await getAppEnvironment(null, 1); let response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v1', versionFromId: initialVersion.id, environmentId: developmentEnv.id, }); expect(response.statusCode).toBe(201); response = await request(app.getHttpServer()) .post(`/api/apps/${application.id}/versions`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ versionName: 'v2', versionFromId: response.body.id, environmentId: developmentEnv.id, }); expect(response.statusCode).toBe(201); const dataSources = await findEntities(DataSource); const dataQueries = await findEntities(DataQuery); expect(dataSources).toHaveLength(3); expect(dataQueries).toHaveLength(3); credentials = await findEntities(Credential); expect([...new Set(credentials.map((c) => c.valueCiphertext))]).toContain('strongPassword'); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); }); }); //deleted the definifion spec while with no versionFrom it will return 500 from server }); describe('DELETE /api/apps/:id/versions/:versionId | Delete version', () => { describe('authorization', () => { it('should not be able to delete app versions if user of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(404); await logout( app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId ); }); it('should able to delete app versions if user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], userType: 'instance', }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); const duplicateVersion = await createApplicationVersion(app, application, { name: 'v123' }); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${duplicateVersion.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); }); it('should be able to delete an app version if group is admin or has app update permission group in same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version1 = await createApplicationVersion(app, application); const version2 = await createApplicationVersion(app, application, { name: 'v2', definition: null }); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); await grantAppPermission(app, application, developerUserGroup.id, { read: false, update: true, delete: false, }); let response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${version1.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']); expect(response.statusCode).toBe(200); response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${version2.id}`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']); expect(response.statusCode).toBe(403); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); it('should not be able to delete app versions if user does not have app update permission group', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users'], organization: adminUserData.organization, }); const loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']); expect(response.statusCode).toBe(403); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); }); it('should not be able to delete released app version', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const version = await createApplicationVersion(app, application); await createApplicationVersion(app, application, { name: 'v2', definition: null }); await updateEntity(App, application.id, { currentVersionId: version.id } as any); const response = await request(app.getHttpServer()) .delete(`/api/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']); expect(response.statusCode).toBe(400); expect(response.body.message).toBe('You cannot delete a released version'); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); }); }); describe('GET /api/apps/:id/versions/:version_id | Get version', () => { describe('authorization', () => { it('should be able to get app version by users having app read permission within same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users'], organization: adminUserData.organization, }); loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const allUserGroup = await findEntityOrFail(GroupPermissions, { name: 'end-user', } as any); await grantAppPermission(app, application, allUserGroup.id, { read: true, update: false, delete: false, }); for (const userData of [adminUserData, developerUserData]) { const response = await request(app.getHttpServer()) .get(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']); expect(response.statusCode).toBe(200); } await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); it('should be able to get app version if the user is super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .get(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); }); it('should not be able to get app versions if user of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const response = await request(app.getHttpServer()) .get(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(404); await logout( app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId ); }); }); describe('PUT /api/apps/:id/versions/:version_id | Update version', () => { it('should be able to update app version if has group admin or app update permission group in same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app, 'admin@tooljet.io'); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); await grantAppPermission(app, application, developerUserGroup.id, { read: false, update: true, delete: false, }); let count = 0; for (const userData of [adminUserData, developerUserData]) { count++; const response = await request(app.getHttpServer()) .put(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']) .send({ name: 'test' + count, definition: { components: {} }, }); expect(response.statusCode).toBe(200); await version.reload(); } await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); }); it('should be able to update the current version without new definition changes, even it is a released versions', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'admin@tooljet.io'); const application = await createApplication(app, { user: adminUserData.user, }); // Version must be on the production (default) environment to be eligible for release // when multi-environment license is enabled const environments = await getAllEnvironments(app, adminUserData.organization.id); const productionEnv = environments.find((env) => env.isDefault); const version = await createApplicationVersion(app, application, { currentEnvironmentId: productionEnv.id, }); let response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser['tokenCookie']) .send({ app: { name: 'new', current_version_id: version.id } }); expect(response.statusCode).toBe(200); response = await request(app.getHttpServer()) .put(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser['tokenCookie']) .send({ is_user_switched_version: true }); expect(response.statusCode).toBe(200); }); it('should not be able to update app version if no app create permission within same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const viewerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users'], organization: adminUserData.organization, }); const loggedUser = await login(app, 'dev@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .put(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send({ name: 'test', definition: { components: {} }, }); expect(response.statusCode).toBe(403); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); }); it('should not be able to update app versions if user of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .put(`/api/v2/apps/${application.id}/versions/${version.id}`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']) .send({ name: 'test', definition: { components: {} }, }); expect(response.statusCode).toBe(404); await logout( app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId ); }); it('should be able to release the app if the version is promoted to production', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { user: adminUserData.user, }); const version = await createApplicationVersion(app, application); const environments = await getAllEnvironments(app, adminUserData.organization.id); for (const appEnvironment of defaultAppEnvironments) { const currentEnv = environments.find((env) => env.name === appEnvironment.name); if (!appEnvironment.isDefault) { const response = await request(app.getHttpServer()) .put(`/api/v2/apps/${application.id}/versions/${version.id}/promote`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ currentEnvironmentId: currentEnv.id, }); expect(response.statusCode).toBe(200); } else { const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: { current_version_id: version.id } }); expect(response.statusCode).toBe(200); } } }); }); }); /* Viewing app on app viewer All org users can launch an app Public apps can be launched by anyone ( even unauthenticated users ) By view app endpoint, we assume the apps/slugs/:id endpoint */ describe('GET /api/apps/slugs/:slug | Get app by slug', () => { it('should be able to fetch app using slug if has read permission within an organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); await createApplicationVersion(app, application); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); await grantAppPermission(app, application, developerUserGroup.id, { read: true, update: true, delete: false, }); // setup app permissions for viewer const viewerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'viewer', } as any); await grantAppPermission(app, application, viewerUserGroup.id, { read: true, update: false, delete: false, }); for (const userData of [adminUserData, developerUserData, viewerUserData]) { const response = await request(app.getHttpServer()) .get('/api/apps/slugs/foo') .set('Cookie', userData['tokenCookie']); expect(response.statusCode).toBe(200); } // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should be able to fetch app using slug if the user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); await createApplicationVersion(app, application); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .get('/api/apps/slugs/foo') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie); expect(response.statusCode).toBe(200); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should not be able to fetch app using slug if member of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()) .get('/api/apps/slugs/foo') .set('Cookie', anotherOrgAdminUserData['tokenCookie']); expect(response.statusCode).toBe(401); await logout(app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId); }); it('should be able to fetch app using slug if a public app ( even if unauthenticated )', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', isPublic: true, }); await createApplicationVersion(app, application); const response = await request(app.getHttpServer()).get('/api/apps/slugs/foo'); expect(response.statusCode).toBe(200); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); }); describe('POST /api/v2/resources/export | Export resources', () => { it('should be able to export app if user has create permission within an organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); await createApplicationVersion(app, application); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); developerUserGroup.appCreate = true; await developerUserGroup.save(); const exportPayload = { app: [{ id: application.id }], organization_id: adminUserData.user.defaultOrganizationId, }; const viewerResponse = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send(exportPayload); expect(viewerResponse.statusCode).toBe(403); for (const userData of [adminUserData, developerUserData]) { const response = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']) .send(exportPayload); expect(response.statusCode).toBe(201); expect(response.body).toBeDefined(); expect(response.body.tooljet_version).toBeDefined(); } // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should be able to export app if user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); await createApplicationVersion(app, application); // setup app permissions for developer const developerUserGroup = await findEntityOrFail(GroupPermissions, { name: 'developer', } as any); developerUserGroup.appCreate = true; await developerUserGroup.save(); const loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.organizationId ); const response = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: [{ id: application.id }], organization_id: adminUserData.user.defaultOrganizationId, }); expect(response.statusCode).toBe(201); expect(response.body).toBeDefined(); expect(response.body.tooljet_version).toBeDefined(); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should not be able to export app if member of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', }); const response = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']) .send({ app: [{ id: application.id }], organization_id: anotherOrgAdminUserData.user.defaultOrganizationId, }); // The ability guard passes (admin has export permission in their org), // but the export service filters by org and fails when the app isn't found expect(response.statusCode).toBe(500); await logout(app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId); }); it('should not be able to export app if it is a public app for an unauthenticated user', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, slug: 'foo', isPublic: true, }); const response = await request(app.getHttpServer()) .post('/api/v2/resources/export') .send({ app: [{ id: application.id }], organization_id: adminUserData.user.defaultOrganizationId, }); expect(response.statusCode).toBe(401); }); }); describe('POST /api/v2/resources/import | Import resources', () => { it('should be able to import app only if user has admin group', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); let loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const organization = adminUserData.organization; const developerUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], organization, }); loggedUser = await login(app, 'developer@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); // Export the app first to get a valid definition for import const exportResponse = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ app: [{ id: application.id }], organization_id: adminUserData.user.defaultOrganizationId, }); expect(exportResponse.statusCode).toBe(201); const exportedAppDefinition = exportResponse.body.app[0].definition; const exportedVersion = exportResponse.body.tooljet_version; for (const userData of [viewerUserData, developerUserData]) { const response = await request(app.getHttpServer()) .post('/api/v2/resources/import') .set('tj-workspace-id', userData.user.defaultOrganizationId) .set('Cookie', userData['tokenCookie']) .send({ organization_id: userData.user.defaultOrganizationId, app: [{ definition: exportedAppDefinition, appName: 'name' }], tooljet_version: exportedVersion, }); expect(response.statusCode).toBe(403); } const response = await request(app.getHttpServer()) .post('/api/v2/resources/import') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ organization_id: adminUserData.user.defaultOrganizationId, app: [{ definition: exportedAppDefinition, appName: 'Imported App' }], tooljet_version: exportedVersion, }); expect(response.statusCode).toBe(201); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); it('should be able to import app only if user is a super admin', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const superAdminUserData = await createUser(app, { email: 'developer@tooljet.io', groups: ['all_users', 'developer'], userType: 'instance', }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); await createApplicationVersion(app, application); // First authenticate admin to export let loggedUser = await login(app); const exportResponse = await request(app.getHttpServer()) .post('/api/v2/resources/export') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ app: [{ id: application.id }], organization_id: adminUserData.user.defaultOrganizationId, }); expect(exportResponse.statusCode).toBe(201); const exportedAppDefinition = exportResponse.body.app[0].definition; const exportedVersion = exportResponse.body.tooljet_version; loggedUser = await login( app, superAdminUserData.user.email, 'password', adminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .post('/api/v2/resources/import') .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ organization_id: adminUserData.user.defaultOrganizationId, app: [{ definition: exportedAppDefinition, appName: 'Imported App' }], tooljet_version: exportedVersion, }); expect(response.statusCode).toBe(201); // Audit log assertions skipped: ResponseInterceptor not registered in test environment }); }); describe('PUT /api/apps/:id/icons | Update app icon', () => { it('should be able to update icon of the app if admin of same organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { user: adminUserData.user, }); const loggedUser = await login(app); adminUserData['tokenCookie'] = loggedUser.tokenCookie; const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}/icons`) .set('tj-workspace-id', adminUserData.user.defaultOrganizationId) .set('Cookie', adminUserData['tokenCookie']) .send({ icon: 'new-icon-name' }); expect(response.statusCode).toBe(200); await application.reload(); expect(application.icon).toBe('new-icon-name'); await logout(app, adminUserData['tokenCookie'], adminUserData.user.defaultOrganizationId); }); it('should not be able to update icon of the app if admin of another organization', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const loggedUser = await login(app, 'another@tooljet.io'); anotherOrgAdminUserData['tokenCookie'] = loggedUser.tokenCookie; const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}/icons`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', anotherOrgAdminUserData['tokenCookie']) .send({ icon: 'new-icon-name' }); expect(response.statusCode).toBe(404); await application.reload(); expect(application.icon).toBe(null); await logout(app, anotherOrgAdminUserData['tokenCookie'], anotherOrgAdminUserData.user.defaultOrganizationId); }); it('should able to update icon of the app if user is super admin', async () => { const superAdminUserData = await createUser(app, { email: 'superadmin@tooljet.io', groups: ['all_users', 'admin'], userType: 'instance', }); const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: anotherOrgAdminUserData.user, }); const loggedUser = await login( app, superAdminUserData.user.email, 'password', anotherOrgAdminUserData.user.defaultOrganizationId ); const response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}/icons`) .set('tj-workspace-id', anotherOrgAdminUserData.user.defaultOrganizationId) .set('Cookie', loggedUser.tokenCookie) .send({ icon: 'new-icon-name' }); expect(response.statusCode).toBe(200); await application.reload(); expect(application.icon).toBe('new-icon-name'); }); it('should not allow custom groups without app create permission to change the icons of apps', async () => { const adminUserData = await createUser(app, { email: 'admin@tooljet.io', groups: ['all_users', 'admin'], }); const application = await createApplication(app, { name: 'name', user: adminUserData.user, }); const developerUserData = await createUser(app, { email: 'dev@tooljet.io', groups: ['all_users', 'developer'], organization: adminUserData.organization, }); let loggedUser = await login(app, 'dev@tooljet.io'); developerUserData['tokenCookie'] = loggedUser.tokenCookie; const viewerUserData = await createUser(app, { email: 'viewer@tooljet.io', groups: ['all_users', 'viewer'], organization: adminUserData.organization, }); loggedUser = await login(app, 'viewer@tooljet.io'); viewerUserData['tokenCookie'] = loggedUser.tokenCookie; let response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}/icons`) .set('tj-workspace-id', developerUserData.user.defaultOrganizationId) .set('Cookie', developerUserData['tokenCookie']) .send({ icon: 'new-icon' }); expect(response.statusCode).toBe(403); response = await request(app.getHttpServer()) .put(`/api/apps/${application.id}/icons`) .set('tj-workspace-id', viewerUserData.user.defaultOrganizationId) .set('Cookie', viewerUserData['tokenCookie']) .send({ icon: 'new-icon' }); expect(response.statusCode).toBe(403); await application.reload(); expect(application.icon).toBe(null); await logout(app, developerUserData['tokenCookie'], developerUserData.user.defaultOrganizationId); await logout(app, viewerUserData['tokenCookie'], viewerUserData.user.defaultOrganizationId); }); }); }); });