b9586769b9
- increase to 0.4.0 - update READMEs and doc
307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
import * as fs from 'fs-extra';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { copyBaseApplicationProject } from '@/utils/app-template';
|
|
|
|
// Mock fs-extra's copy function to skip copying base template (not available during tests)
|
|
jest.mock('fs-extra', () => {
|
|
const actual = jest.requireActual('fs-extra');
|
|
return {
|
|
...actual,
|
|
copy: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
});
|
|
|
|
describe('copyBaseApplicationProject', () => {
|
|
let testAppDirectory: string;
|
|
|
|
beforeEach(async () => {
|
|
// Create a unique temp directory for each test
|
|
testAppDirectory = join(
|
|
tmpdir(),
|
|
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
await fs.ensureDir(testAppDirectory);
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up temp directory after each test
|
|
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
|
|
await fs.remove(testAppDirectory);
|
|
}
|
|
});
|
|
|
|
it('should create the correct folder structure with src/app/', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
// Verify src/app/ folder exists
|
|
const srcAppPath = join(testAppDirectory, 'src');
|
|
expect(await fs.pathExists(srcAppPath)).toBe(true);
|
|
|
|
// Verify application.config.ts exists in src/app/
|
|
const appConfigPath = join(srcAppPath, 'application.config.ts');
|
|
expect(await fs.pathExists(appConfigPath)).toBe(true);
|
|
|
|
// Verify default-function.role.ts exists in src/app/
|
|
const roleConfigPath = join(srcAppPath, 'default-function.role.ts');
|
|
expect(await fs.pathExists(roleConfigPath)).toBe(true);
|
|
});
|
|
|
|
it('should create package.json with correct content', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const packageJsonPath = join(testAppDirectory, 'package.json');
|
|
expect(await fs.pathExists(packageJsonPath)).toBe(true);
|
|
|
|
const packageJson = await fs.readJson(packageJsonPath);
|
|
expect(packageJson.name).toBe('my-test-app');
|
|
expect(packageJson.version).toBe('0.1.0');
|
|
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
|
|
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
|
|
});
|
|
|
|
it('should create .gitignore file', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const gitignorePath = join(testAppDirectory, '.gitignore');
|
|
expect(await fs.pathExists(gitignorePath)).toBe(true);
|
|
|
|
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
|
|
expect(gitignoreContent).toContain('/node_modules');
|
|
expect(gitignoreContent).toContain('generated');
|
|
});
|
|
|
|
it('should create yarn.lock file', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const yarnLockPath = join(testAppDirectory, 'yarn.lock');
|
|
expect(await fs.pathExists(yarnLockPath)).toBe(true);
|
|
|
|
const yarnLockContent = await fs.readFile(yarnLockPath, 'utf8');
|
|
expect(yarnLockContent).toContain('yarn lockfile v1');
|
|
});
|
|
|
|
it('should create application.config.ts with defineApp and correct values', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const appConfigPath = join(
|
|
testAppDirectory,
|
|
'src',
|
|
'application.config.ts',
|
|
);
|
|
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
|
|
|
// Verify it uses defineApp
|
|
expect(appConfigContent).toContain(
|
|
"import { defineApp } from 'twenty-sdk'",
|
|
);
|
|
expect(appConfigContent).toContain('export default defineApp({');
|
|
|
|
// Verify it imports the role identifier
|
|
expect(appConfigContent).toContain(
|
|
"import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role'",
|
|
);
|
|
|
|
// Verify display name and description
|
|
expect(appConfigContent).toContain("displayName: 'My Test App'");
|
|
expect(appConfigContent).toContain("description: 'A test application'");
|
|
|
|
// Verify it has a universalIdentifier (UUID format)
|
|
expect(appConfigContent).toMatch(
|
|
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
|
|
);
|
|
|
|
// Verify it references the role
|
|
expect(appConfigContent).toContain(
|
|
'functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
|
|
);
|
|
});
|
|
|
|
it('should create default-function.role.ts with defineRole and correct values', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const roleConfigPath = join(
|
|
testAppDirectory,
|
|
'src',
|
|
'default-function.role.ts',
|
|
);
|
|
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
|
|
|
|
// Verify it uses defineRole
|
|
expect(roleConfigContent).toContain(
|
|
"import { defineRole } from 'twenty-sdk'",
|
|
);
|
|
expect(roleConfigContent).toContain('export default defineRole({');
|
|
|
|
// Verify it exports the universal identifier constant
|
|
expect(roleConfigContent).toContain(
|
|
'export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
|
|
);
|
|
|
|
// Verify role label includes app name
|
|
expect(roleConfigContent).toContain(
|
|
"label: 'My Test App default function role'",
|
|
);
|
|
|
|
// Verify default permissions
|
|
expect(roleConfigContent).toContain('canReadAllObjectRecords: true');
|
|
expect(roleConfigContent).toContain('canUpdateAllObjectRecords: true');
|
|
expect(roleConfigContent).toContain('canSoftDeleteAllObjectRecords: true');
|
|
expect(roleConfigContent).toContain('canDestroyAllObjectRecords: false');
|
|
|
|
// Verify it has a universalIdentifier (UUID format)
|
|
expect(roleConfigContent).toMatch(
|
|
/universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER/,
|
|
);
|
|
});
|
|
|
|
it('should call fs.copy to copy base application template', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: 'A test application',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
// Verify fs.copy was called with correct destination
|
|
expect(fs.copy).toHaveBeenCalledTimes(1);
|
|
expect(fs.copy).toHaveBeenCalledWith(
|
|
expect.stringContaining('base-application'),
|
|
testAppDirectory,
|
|
);
|
|
});
|
|
|
|
it('should handle empty description', async () => {
|
|
await copyBaseApplicationProject({
|
|
appName: 'my-test-app',
|
|
appDisplayName: 'My Test App',
|
|
appDescription: '',
|
|
appDirectory: testAppDirectory,
|
|
});
|
|
|
|
const appConfigPath = join(
|
|
testAppDirectory,
|
|
'src',
|
|
'application.config.ts',
|
|
);
|
|
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
|
|
|
expect(appConfigContent).toContain("description: ''");
|
|
});
|
|
|
|
it('should generate unique UUIDs for each application', async () => {
|
|
// Create first app
|
|
const firstAppDir = join(testAppDirectory, 'app1');
|
|
await fs.ensureDir(firstAppDir);
|
|
await copyBaseApplicationProject({
|
|
appName: 'app-one',
|
|
appDisplayName: 'App One',
|
|
appDescription: 'First app',
|
|
appDirectory: firstAppDir,
|
|
});
|
|
|
|
// Create second app
|
|
const secondAppDir = join(testAppDirectory, 'app2');
|
|
await fs.ensureDir(secondAppDir);
|
|
await copyBaseApplicationProject({
|
|
appName: 'app-two',
|
|
appDisplayName: 'App Two',
|
|
appDescription: 'Second app',
|
|
appDirectory: secondAppDir,
|
|
});
|
|
|
|
// Read both app configs
|
|
const firstAppConfig = await fs.readFile(
|
|
join(firstAppDir, 'src', 'application.config.ts'),
|
|
'utf8',
|
|
);
|
|
const secondAppConfig = await fs.readFile(
|
|
join(secondAppDir, 'src', 'application.config.ts'),
|
|
'utf8',
|
|
);
|
|
|
|
// Extract UUIDs using regex
|
|
const uuidRegex =
|
|
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
|
const firstUuid = firstAppConfig.match(uuidRegex)?.[1];
|
|
const secondUuid = secondAppConfig.match(uuidRegex)?.[1];
|
|
|
|
expect(firstUuid).toBeDefined();
|
|
expect(secondUuid).toBeDefined();
|
|
expect(firstUuid).not.toBe(secondUuid);
|
|
});
|
|
|
|
it('should generate unique role UUIDs for each application', async () => {
|
|
// Create first app
|
|
const firstAppDir = join(testAppDirectory, 'app1');
|
|
await fs.ensureDir(firstAppDir);
|
|
await copyBaseApplicationProject({
|
|
appName: 'app-one',
|
|
appDisplayName: 'App One',
|
|
appDescription: 'First app',
|
|
appDirectory: firstAppDir,
|
|
});
|
|
|
|
// Create second app
|
|
const secondAppDir = join(testAppDirectory, 'app2');
|
|
await fs.ensureDir(secondAppDir);
|
|
await copyBaseApplicationProject({
|
|
appName: 'app-two',
|
|
appDisplayName: 'App Two',
|
|
appDescription: 'Second app',
|
|
appDirectory: secondAppDir,
|
|
});
|
|
|
|
// Read both role configs
|
|
const firstRoleConfig = await fs.readFile(
|
|
join(firstAppDir, 'src', 'default-function.role.ts'),
|
|
'utf8',
|
|
);
|
|
const secondRoleConfig = await fs.readFile(
|
|
join(secondAppDir, 'src', 'default-function.role.ts'),
|
|
'utf8',
|
|
);
|
|
|
|
// Extract UUIDs using regex
|
|
const uuidRegex =
|
|
/DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
|
const firstUuid = firstRoleConfig.match(uuidRegex)?.[1];
|
|
const secondUuid = secondRoleConfig.match(uuidRegex)?.[1];
|
|
|
|
expect(firstUuid).toBeDefined();
|
|
expect(secondUuid).toBeDefined();
|
|
expect(firstUuid).not.toBe(secondUuid);
|
|
});
|
|
});
|