Feature: Import/Export Modules External API (#16300)

* feat(external-apis): add LIST_MODULES, EXPORT_MODULE, IMPORT_MODULE feature keys, DTOs, and ability grants

* feat(external-apis): add CE stub modules controller and register in module

* feat(external-apis): add end-to-end tests for ExternalApisModulesController

* refactor(external-apis): simplify exportModule method signature in ExternalApisModulesController

* feat(external-apis): enhance tests for ExternalApisModulesController with additional cases for non-existent UUIDs

* feat(external-apis): update exportModule method to include exportTjdb parameter and enhance tests for its functionality

* feat(external-apis): enhance tooljet_database import schema validation and add module import/export helpers

* feat(external-apis): add tests for ExternalApisModulesController in starter and CE plans with appropriate status checks

* feat(external-apis): add tests for module and app import endpoints to validate JSON rejection
This commit is contained in:
Shantanu Mane 2026-05-08 21:16:38 +05:30 committed by GitHub
parent 2b608ed64b
commit 0fb732600a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 643 additions and 9 deletions

View file

@ -112,7 +112,7 @@ export function ValidateTooljetDatabaseSchema(validationOptions?: ValidationOpti
}
export function ValidateTooljetDatabaseImportSchema(validationOptions?: ValidationOptions) {
return function (object: AppImportRequestDto, propertyName: string) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,

View file

@ -31,6 +31,9 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
FEATURE_KEY.AUTO_RELEASE_APP,
FEATURE_KEY.GENERATE_PAT,
FEATURE_KEY.VALIDATE_PAT_SESSION,
FEATURE_KEY.LIST_MODULES,
FEATURE_KEY.EXPORT_MODULE,
FEATURE_KEY.IMPORT_MODULE,
],
User
);

View file

@ -77,5 +77,17 @@ export const FEATURES: FeaturesConfig = {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.LIST_MODULES]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.EXPORT_MODULE]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.IMPORT_MODULE]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
},
};

View file

@ -17,6 +17,9 @@ export enum FEATURE_KEY {
EXPORT_APP = 'EXPORT_APP',
GENERATE_PAT = 'GENERATE_PAT',
VALIDATE_PAT_SESSION = 'VALIDATE_PAT_SESSION',
LIST_MODULES = 'LIST_MODULES',
EXPORT_MODULE = 'EXPORT_MODULE',
IMPORT_MODULE = 'IMPORT_MODULE',
}
export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';

View file

@ -0,0 +1,22 @@
import { MODULES } from '@modules/app/constants/modules';
import { InitModule } from '@modules/app/decorators/init-module';
import { Controller, UseGuards } from '@nestjs/common';
import { FeatureAbilityGuard } from '../ability/guard';
import { ModuleImportRequestDto, WorkspaceModulesResponseDto } from '../dto';
@Controller('ext')
@InitModule(MODULES.EXTERNAL_APIS)
@UseGuards(FeatureAbilityGuard)
export class ExternalApisModulesController {
getAllWorkspaceModules(workspaceId: string): Promise<WorkspaceModulesResponseDto> {
throw new Error('Method not implemented.');
}
exportModule(moduleId: string, workspaceId: string, exportTjdb: boolean): Promise<any> {
throw new Error('Method not implemented.');
}
importModule(workspaceId: string, importresources: ModuleImportRequestDto): Promise<{ message: string }> {
throw new Error('Method not implemented.');
}
}

View file

@ -293,3 +293,43 @@ export class ValidatePATSessionDto {
@IsString()
accessToken: string;
}
export class WorkspaceModuleDto {
id: string;
name: string;
icon: string;
slug: string;
isPublic: boolean;
createdAt: Date;
updatedAt: Date;
}
export class WorkspaceModulesResponseDto {
modules: WorkspaceModuleDto[];
total: number;
}
export class ModuleImportDto {
@IsDefined()
@IsObject()
definition: any;
}
export class ModuleImportRequestDto {
@IsString()
tooljet_version: string;
@IsOptional()
app?: ModuleImportDto[];
@IsOptional()
@IsString()
appName?: string;
@IsOptional()
@Transform(TjdbSchemaToLatestVersion)
@ValidateNested({ each: true })
@Type(() => ImportTooljetDatabaseDto)
@ValidateTooljetDatabaseImportSchema({ each: true })
tooljet_database?: ImportTooljetDatabaseDto[];
}

View file

@ -23,13 +23,19 @@ import { UserRepository } from '@modules/users/repositories/repository';
export class ExternalApiModule extends SubModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }, isMainImport: boolean = false): Promise<DynamicModule> {
const { ExternalApisController, ExternalApisService, ExternalApiUtilService, ExternalApisAppsController } =
await this.getProviders(configs, 'external-apis', [
'controller',
'service',
'util.service',
'controllers/apps.controller',
]);
const {
ExternalApisController,
ExternalApisService,
ExternalApiUtilService,
ExternalApisAppsController,
ExternalApisModulesController,
} = await this.getProviders(configs, 'external-apis', [
'controller',
'service',
'util.service',
'controllers/apps.controller',
'controllers/modules.controller',
]);
return {
module: ExternalApiModule,
@ -61,7 +67,7 @@ export class ExternalApiModule extends SubModule {
UserRepository,
AppsRepository,
],
controllers: isMainImport ? [ExternalApisController, ExternalApisAppsController] : [],
controllers: isMainImport ? [ExternalApisController, ExternalApisAppsController, ExternalApisModulesController] : [],
exports: [ExternalApiUtilService],
};
}

View file

@ -21,6 +21,9 @@ interface Features {
[FEATURE_KEY.EXPORT_APP]: FeatureConfig;
[FEATURE_KEY.GENERATE_PAT]: FeatureConfig;
[FEATURE_KEY.VALIDATE_PAT_SESSION]: FeatureConfig;
[FEATURE_KEY.LIST_MODULES]: FeatureConfig;
[FEATURE_KEY.EXPORT_MODULE]: FeatureConfig;
[FEATURE_KEY.IMPORT_MODULE]: FeatureConfig;
}
export interface FeaturesConfig {

View file

@ -0,0 +1,545 @@
/**
* @group platform
*/
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import {
createUser,
initTestApp,
closeTestApp,
createApplication,
createApplicationVersion,
} from 'test-helper';
import { APP_TYPES } from '@modules/apps/constants';
jest.setTimeout(120_000);
// Token is read from .env.test at runtime by ConfigService — read after env is loaded.
const getExtAuth = () => `Basic ${process.env.EXTERNAL_API_ACCESS_TOKEN}`;
// A valid UUID that will never exist in the test database.
const NONEXISTENT_UUID = '00000000-0000-0000-0000-000000000001';
// Helper: export a module and return the full export body.
async function exportModule(
server: ReturnType<INestApplication['getHttpServer']>,
orgId: string,
moduleId: string,
query = ''
) {
const res = await request(server)
.post(`/api/ext/export/workspace/${orgId}/modules/${moduleId}${query}`)
.set('Authorization', getExtAuth())
.expect(201);
return res.body;
}
// Helper: import a module and return the response body.
async function importModule(
server: ReturnType<INestApplication['getHttpServer']>,
orgId: string,
payload: Record<string, unknown>
) {
const res = await request(server)
.post(`/api/ext/import/workspace/${orgId}/modules`)
.set('Authorization', getExtAuth())
.send(payload)
.expect(201);
return res.body;
}
// Helper: list modules in a workspace.
async function listModules(server: ReturnType<INestApplication['getHttpServer']>, orgId: string) {
const res = await request(server)
.get(`/api/ext/workspace/${orgId}/modules`)
.set('Authorization', getExtAuth())
.expect(200);
return res.body as { modules: any[]; total: number };
}
describe('ExternalApisModulesController (EE enterprise)', () => {
let app: INestApplication;
beforeAll(async () => {
({ app } = await initTestApp({ edition: 'ee', plan: 'enterprise' }));
});
afterEach(() => {
jest.resetAllMocks();
});
afterAll(async () => {
await closeTestApp(app);
}, 60000);
// ---------------------------------------------------------------------------
// GET /api/ext/workspace/:workspaceId/modules
// ---------------------------------------------------------------------------
describe('GET /api/ext/workspace/:workspaceId/modules', () => {
it('returns 403 without Authorization header', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.get(`/api/ext/workspace/${user.defaultOrganizationId}/modules`)
.expect(403);
});
it('returns 400 for non-UUID workspaceId', async () => {
await request(app.getHttpServer())
.get('/api/ext/workspace/not-a-uuid/modules')
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns 400 for a valid UUID workspace that does not exist', async () => {
await request(app.getHttpServer())
.get(`/api/ext/workspace/${NONEXISTENT_UUID}/modules`)
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns empty list when workspace has no modules', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const res = await request(app.getHttpServer())
.get(`/api/ext/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.expect(200);
expect(res.body.modules).toEqual([]);
expect(res.body.total).toBe(0);
});
it('returns correct response shape for each module', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await createApplication(app, { name: 'Shape Module', user, type: APP_TYPES.MODULE });
const res = await request(app.getHttpServer())
.get(`/api/ext/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.expect(200);
expect(res.body.total).toBe(1);
const mod = res.body.modules[0];
expect(mod).toHaveProperty('id');
expect(mod).toHaveProperty('name', 'Shape Module');
expect(mod).toHaveProperty('slug');
expect(mod).toHaveProperty('icon');
expect(mod).toHaveProperty('isPublic');
expect(mod).toHaveProperty('createdAt');
expect(mod).toHaveProperty('updatedAt');
});
it('returns only module-type apps (excludes front-end apps)', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const orgId = user.defaultOrganizationId;
await createApplication(app, { name: 'Front-end app', user, type: APP_TYPES.FRONT_END });
const module1 = await createApplication(app, { name: 'Auth Module', user, type: APP_TYPES.MODULE }, false);
const module2 = await createApplication(app, { name: 'Payment Module', user, type: APP_TYPES.MODULE }, false);
const res = await request(app.getHttpServer())
.get(`/api/ext/workspace/${orgId}/modules`)
.set('Authorization', getExtAuth())
.expect(200);
expect(res.body.total).toBe(2);
const names = res.body.modules.map((m: any) => m.name);
expect(names).toContain(module1.name);
expect(names).toContain(module2.name);
expect(names).not.toContain('Front-end app');
});
it('returns modules belonging to the given workspace only', async () => {
const { user: user1 } = await createUser(app, { email: 'user1@tooljet.io' });
const { user: user2 } = await createUser(app, { email: 'user2@tooljet.io' });
await createApplication(app, { name: 'Module A', user: user1, type: APP_TYPES.MODULE });
await createApplication(app, { name: 'Module B', user: user2, type: APP_TYPES.MODULE });
const res = await request(app.getHttpServer())
.get(`/api/ext/workspace/${user1.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.expect(200);
expect(res.body.total).toBe(1);
expect(res.body.modules[0].name).toBe('Module A');
});
});
// ---------------------------------------------------------------------------
// POST /api/ext/export/workspace/:workspaceId/modules/:moduleId
// ---------------------------------------------------------------------------
describe('POST /api/ext/export/workspace/:workspaceId/modules/:moduleId', () => {
it('returns 403 without Authorization header', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const mod = await createApplication(app, { name: 'M', user, type: APP_TYPES.MODULE });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user.defaultOrganizationId}/modules/${mod.id}`)
.expect(403);
});
it('returns 400 for non-UUID moduleId', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user.defaultOrganizationId}/modules/not-a-uuid`)
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns 400 for a valid UUID moduleId that does not exist', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user.defaultOrganizationId}/modules/${NONEXISTENT_UUID}`)
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns 400 when module does not belong to workspace', async () => {
const { user: user1 } = await createUser(app, { email: 'user1@tooljet.io' });
const { user: user2 } = await createUser(app, { email: 'user2@tooljet.io' });
const mod = await createApplication(app, { name: 'M', user: user1, type: APP_TYPES.MODULE });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user2.defaultOrganizationId}/modules/${mod.id}`)
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns 400 when app exists but is not a module type', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const frontendApp = await createApplication(app, { name: 'FE', user, type: APP_TYPES.FRONT_END });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user.defaultOrganizationId}/modules/${frontendApp.id}`)
.set('Authorization', getExtAuth())
.expect(400);
});
it('exports module and returns definition with tooljet_version', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const mod = await createApplication(app, { name: 'Auth Module', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const body = await exportModule(app.getHttpServer(), user.defaultOrganizationId, mod.id);
expect(body).toHaveProperty('tooljet_version');
expect(typeof body.tooljet_version).toBe('string');
expect(body).toHaveProperty('app');
expect(Array.isArray(body.app)).toBe(true);
expect(body.app.length).toBeGreaterThan(0);
expect(body.app[0]).toHaveProperty('definition');
});
it('excludes TJDB from export when exportTJDB=false', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const mod = await createApplication(app, { name: 'Auth Module', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const body = await exportModule(app.getHttpServer(), user.defaultOrganizationId, mod.id, '?exportTJDB=false');
expect(body).not.toHaveProperty('tooljet_database');
});
it('does not exclude TJDB when exportTJDB=true explicitly', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const mod = await createApplication(app, { name: 'Auth Module', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
// Should not error — TJDB inclusion is allowed even when the module has no tables
const body = await exportModule(app.getHttpServer(), user.defaultOrganizationId, mod.id, '?exportTJDB=true');
expect(body).toHaveProperty('tooljet_version');
expect(body).toHaveProperty('app');
});
});
// ---------------------------------------------------------------------------
// POST /api/ext/import/workspace/:workspaceId/modules
// ---------------------------------------------------------------------------
describe('POST /api/ext/import/workspace/:workspaceId/modules', () => {
it('returns 403 without Authorization header', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.expect(403);
});
it('returns 400 for non-UUID workspaceId', async () => {
await request(app.getHttpServer())
.post('/api/ext/import/workspace/not-a-uuid/modules')
.set('Authorization', getExtAuth())
.expect(400);
});
it('returns 400 for a valid UUID workspace that does not exist', async () => {
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${NONEXISTENT_UUID}/modules`)
.set('Authorization', getExtAuth())
.send({ tooljet_version: '1.0.0', app: [] })
.expect(400);
});
it('returns 400 when tooljet_version is missing from request body', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({ app: [] })
.expect(400);
});
it('returns 400 when tooljet_version exceeds current version', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({ tooljet_version: '999.0.0', app: [] })
.expect(400);
});
it('accepts import body with only tooljet_version (no app, no tooljet_database)', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const res = await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({ tooljet_version: '1.0.0' })
.expect(201);
expect(res.body.message).toBe('Module imported successfully.');
});
it('accepts import body with tooljet_database as empty array', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const res = await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({ tooljet_version: '1.0.0', tooljet_database: [] })
.expect(201);
expect(res.body.message).toBe('Module imported successfully.');
});
it('returns 400 when tooljet_database entry is missing the id field', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: '1.0.0',
tooljet_database: [{ table_name: 'test_table', schema: { columns: [] } }],
})
.expect(400);
});
it('returns 400 when tooljet_database entry id is not a valid UUID', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: '1.0.0',
tooljet_database: [{ id: 'not-a-uuid', table_name: 'test_table', schema: { columns: [] } }],
})
.expect(400);
});
it('returns 400 when tooljet_database entry is missing table_name', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: '1.0.0',
tooljet_database: [{ id: NONEXISTENT_UUID, schema: { columns: [] } }],
})
.expect(400);
});
it('imports module, returns success message, and module appears in workspace listing', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const orgId = user.defaultOrganizationId;
const mod = await createApplication(app, { name: 'Source Module', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const exportBody = await exportModule(app.getHttpServer(), orgId, mod.id);
const importRes = await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${orgId}/modules`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: exportBody.tooljet_version,
app: exportBody.app,
tooljet_database: exportBody.tooljet_database ?? [],
})
.expect(201);
expect(importRes.body.message).toBe('Module imported successfully.');
// Verify the imported module materialised in the workspace (total goes from 1 → 2)
const listing = await listModules(app.getHttpServer(), orgId);
expect(listing.total).toBe(2);
});
it('imports module with appName override and reflects the new name in workspace listing', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const orgId = user.defaultOrganizationId;
const mod = await createApplication(app, { name: 'Original Name', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const exportBody = await exportModule(app.getHttpServer(), orgId, mod.id);
await importModule(app.getHttpServer(), orgId, {
tooljet_version: exportBody.tooljet_version,
appName: 'Renamed Module',
app: exportBody.app,
tooljet_database: exportBody.tooljet_database ?? [],
});
const listing = await listModules(app.getHttpServer(), orgId);
const names = listing.modules.map((m: any) => m.name);
expect(names).toContain('Renamed Module');
});
it('imports into a different workspace than the source', async () => {
const { user: user1 } = await createUser(app, { email: 'user1@tooljet.io' });
const { user: user2 } = await createUser(app, { email: 'user2@tooljet.io' });
const mod = await createApplication(app, { name: 'Portable Module', user: user1, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const exportBody = await exportModule(app.getHttpServer(), user1.defaultOrganizationId, mod.id);
await importModule(app.getHttpServer(), user2.defaultOrganizationId, {
tooljet_version: exportBody.tooljet_version,
appName: 'Portable Module',
app: exportBody.app,
tooljet_database: exportBody.tooljet_database ?? [],
});
const listing = await listModules(app.getHttpServer(), user2.defaultOrganizationId);
expect(listing.total).toBe(1);
expect(listing.modules[0].name).toBe('Portable Module');
});
it('returns 400 when a front-end app JSON is sent to the module import endpoint', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const orgId = user.defaultOrganizationId;
const frontendApp = await createApplication(app, { name: 'Frontend App', user, type: APP_TYPES.FRONT_END });
await createApplicationVersion(app, frontendApp);
// Export the front-end app via the apps export endpoint
const exportRes = await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${orgId}/apps/${frontendApp.id}`)
.set('Authorization', getExtAuth())
.expect(201);
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${orgId}/modules`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: exportRes.body.tooljet_version ?? '1.0.0',
app: exportRes.body.app,
tooljet_database: exportRes.body.tooljet_database ?? [],
})
.expect(400);
});
});
// ---------------------------------------------------------------------------
// POST /api/ext/import/workspace/:workspaceId/apps — module JSON rejection
// ---------------------------------------------------------------------------
describe('POST /api/ext/import/workspace/:workspaceId/apps', () => {
it('returns 400 when a module JSON is sent to the app import endpoint', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
const orgId = user.defaultOrganizationId;
const mod = await createApplication(app, { name: 'Source Module', user, type: APP_TYPES.MODULE });
await createApplicationVersion(app, mod);
const exportBody = await exportModule(app.getHttpServer(), orgId, mod.id);
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${orgId}/apps`)
.set('Authorization', getExtAuth())
.send({
tooljet_version: exportBody.tooljet_version,
app: exportBody.app,
tooljet_database: exportBody.tooljet_database ?? [],
})
.expect(400);
});
});
});
describe('ExternalApisModulesController (EE plan: starter)', () => {
let app: INestApplication;
beforeAll(async () => {
({ app } = await initTestApp({ edition: 'ee', plan: 'starter' }));
});
afterEach(() => {
jest.resetAllMocks();
});
afterAll(async () => {
await closeTestApp(app);
}, 60000);
it('GET /api/ext/workspace/:workspaceId/modules returns 403 — externalApi not included in starter plan', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.get(`/api/ext/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.expect(451);
});
});
describe('ExternalApisModulesController (CE)', () => {
let app: INestApplication;
beforeAll(async () => {
({ app } = await initTestApp({ edition: 'ce' }));
});
afterEach(() => {
jest.resetAllMocks();
});
afterAll(async () => {
await closeTestApp(app);
}, 60000);
it('GET /api/ext/workspace/:workspaceId/modules returns 404 — route not registered on CE', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.get(`/api/ext/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.expect(404);
});
it('POST /api/ext/export/workspace/:workspaceId/modules/:moduleId returns 404 — route not registered on CE', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/export/workspace/${user.defaultOrganizationId}/modules/${NONEXISTENT_UUID}`)
.set('Authorization', getExtAuth())
.expect(404);
});
it('POST /api/ext/import/workspace/:workspaceId/modules returns 404 — route not registered on CE', async () => {
const { user } = await createUser(app, { email: 'admin@tooljet.io' });
await request(app.getHttpServer())
.post(`/api/ext/import/workspace/${user.defaultOrganizationId}/modules`)
.set('Authorization', getExtAuth())
.send({ tooljet_version: '1.0.0' })
.expect(404);
});
});