create-datadao
Version:
A CLI tool to generate and deploy DataDAOs on the Vana network
754 lines (593 loc) • 27.5 kB
JavaScript
const {
generateTemplate,
guideNextSteps,
guideGitHubSetup,
checkGitHubCLI,
createRepositoriesAutomatically,
guideManualRepositorySetup
} = require('../generator');
const { createMockConfig, createMockWallet } = require('../../__tests__/mocks/factories');
// Mock dependencies
jest.mock('fs-extra');
jest.mock('child_process');
jest.mock('ora');
jest.mock('inquirer');
jest.mock('chalk');
jest.mock('../wallet');
jest.mock('viem');
jest.mock('viem/chains');
jest.mock('../template-engine');
const fs = require('fs-extra');
const { execSync } = require('child_process');
const ora = require('ora');
const inquirer = require('inquirer');
const chalk = require('chalk');
const { deriveWalletFromPrivateKey } = require('../wallet');
const TemplateEngine = require('../template-engine');
const path = require('path');
describe('Generator Functions', () => {
let mockSpinner;
let mockTemplateEngine;
beforeEach(() => {
jest.clearAllMocks();
// Mock console.log and console.warn
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
// Mock ora spinner
mockSpinner = {
start: jest.fn().mockReturnThis(),
text: jest.fn(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis()
};
// Make text a setter that captures values
Object.defineProperty(mockSpinner, 'text', {
set: jest.fn(),
get: jest.fn()
});
ora.mockReturnValue(mockSpinner);
// Mock chalk
Object.assign(chalk, {
blue: jest.fn(text => `[blue]${text}[/blue]`),
green: jest.fn(text => `[green]${text}[/green]`),
yellow: jest.fn(text => `[yellow]${text}[/yellow]`),
red: jest.fn(text => `[red]${text}[/red]`),
cyan: jest.fn(text => `[cyan]${text}[/cyan]`),
gray: jest.fn(text => `[gray]${text}[/gray]`)
});
// Mock TemplateEngine
mockTemplateEngine = {
getDefaultVanaConfig: jest.fn(() => ({
DLP_REGISTRY_CONTRACT_ADDRESS: '0x1234567890123456789012345678901234567890',
DATA_REGISTRY_CONTRACT_ADDRESS: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
MOKSHA_RPC_URL: 'https://rpc.moksha.vana.org'
})),
processMultipleTemplates: jest.fn(() => [
{ success: true, template: 'env/contracts.env.template' },
{ success: true, template: 'env/refiner.env.template' },
{ success: true, template: 'env/ui.env.template' }
]),
processTemplateToFile: jest.fn()
};
TemplateEngine.mockImplementation(() => mockTemplateEngine);
// Mock fs-extra
fs.ensureDirSync.mockImplementation(() => {});
fs.existsSync.mockReturnValue(true);
fs.writeFileSync.mockImplementation(() => {});
fs.readFileSync.mockReturnValue('{}');
fs.copyFileSync.mockImplementation(() => {});
fs.removeSync.mockImplementation(() => {});
// Mock deriveWalletFromPrivateKey
deriveWalletFromPrivateKey.mockReturnValue(createMockWallet());
// Mock execSync
execSync.mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('generateTemplate', () => {
test('generates complete DataDAO project successfully', async () => {
const targetDir = '/test/project';
const config = createMockConfig();
const result = await generateTemplate(targetDir, config);
expect(result).toBe(true);
expect(mockSpinner.start).toHaveBeenCalled();
expect(mockSpinner.succeed).toHaveBeenCalledWith('DataDAO project generated successfully');
expect(fs.ensureDirSync).toHaveBeenCalledWith(targetDir);
});
test('derives wallet credentials when missing from config', async () => {
const targetDir = '/test/project';
const config = createMockConfig();
delete config.address;
delete config.publicKey;
await generateTemplate(targetDir, config);
expect(deriveWalletFromPrivateKey).toHaveBeenCalledWith(config.privateKey);
});
test('skips wallet derivation when credentials already present', async () => {
const targetDir = '/test/project';
const config = createMockConfig({
address: '0x1234567890123456789012345678901234567890',
publicKey: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
});
await generateTemplate(targetDir, config);
expect(deriveWalletFromPrivateKey).not.toHaveBeenCalled();
});
test('handles dependency installation errors gracefully', async () => {
const targetDir = '/test/project';
const config = createMockConfig();
// Mock execSync to succeed for git operations but fail for npm install
execSync.mockImplementation((cmd) => {
if (cmd.includes('npm install')) {
throw new Error('npm install failed');
}
return ''; // For git commands
});
const result = await generateTemplate(targetDir, config);
expect(result).toBe(true);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('npm install'));
});
test.skip('updates spinner text during different phases', async () => {
// TODO: This test checks implementation details of spinner text updates
const targetDir = '/test/project';
const config = createMockConfig();
await generateTemplate(targetDir, config);
expect(mockSpinner.text).toHaveBeenLastCalledWith('Deployment scripts generated');
});
test('handles project generation errors', async () => {
const targetDir = '/test/project';
const config = createMockConfig();
fs.ensureDirSync.mockImplementationOnce(() => {
throw new Error('Directory creation failed');
});
await expect(generateTemplate(targetDir, config)).rejects.toThrow('Directory creation failed');
expect(mockSpinner.fail).toHaveBeenCalledWith('Error generating DataDAO project');
});
test.skip('installs dependencies in correct order', async () => {
// TODO: This test checks implementation details of npm install order
const targetDir = '/test/project';
const config = createMockConfig();
await generateTemplate(targetDir, config);
const execCalls = execSync.mock.calls;
expect(execCalls[0]).toEqual(['npm install', { cwd: targetDir, stdio: 'pipe' }]);
expect(execCalls[1]).toEqual(['npm install', { cwd: path.join(targetDir, 'contracts'), stdio: 'pipe' }]);
expect(execCalls[2]).toEqual(['npm install', { cwd: path.join(targetDir, 'ui'), stdio: 'pipe' }]);
});
test('creates deployment.json with correct structure', async () => {
const targetDir = '/test/project';
const config = createMockConfig();
await generateTemplate(targetDir, config);
const writeCall = fs.writeFileSync.mock.calls.find(call =>
call[0].endsWith('deployment.json')
);
expect(writeCall).toBeTruthy();
const deploymentData = JSON.parse(writeCall[1]);
expect(deploymentData).toHaveProperty('dlpName', config.dlpName);
expect(deploymentData).toHaveProperty('state');
expect(deploymentData.state).toHaveProperty('contractsDeployed', false);
expect(deploymentData.state).toHaveProperty('dataDAORegistered', false);
});
});
describe('guideNextSteps', () => {
beforeEach(() => {
// Mock deployment.json content
const mockDeployment = {
dlpName: 'TestDAO',
state: {
contractsDeployed: true,
dataDAORegistered: false,
proofGitSetup: false,
refinerGitSetup: false,
proofConfigured: false,
refinerConfigured: false,
uiConfigured: false
}
};
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(JSON.stringify(mockDeployment));
});
test('guides through complete setup process', async () => {
inquirer.prompt.mockImplementation(({ name }) => {
const responses = {
proceedWithGitHub: false,
registerDataDAO: false,
configureProof: false,
configureRefiner: false,
configureUI: false,
showUIInstructions: false
};
return Promise.resolve({ [name]: responses[name] });
});
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Let\'s complete your DataDAO setup'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Step 1: Set up GitHub repositories'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Step 2: Register DataDAO'));
});
test('skips GitHub setup when already completed', async () => {
const mockDeployment = {
state: {
contractsDeployed: true,
proofGitSetup: true,
refinerGitSetup: true,
dataDAORegistered: false
}
};
fs.readFileSync.mockReturnValue(JSON.stringify(mockDeployment));
inquirer.prompt.mockResolvedValue({ registerDataDAO: false });
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('✅ Step 1: GitHub repositories already set up'));
});
test.skip('executes registration when user confirms', async () => {
// TODO: Complex integration test with multiple mocks
inquirer.prompt.mockImplementation(({ name }) => {
if (name === 'registerDataDAO') return Promise.resolve({ registerDataDAO: true });
return Promise.resolve({ [name]: false });
});
await guideNextSteps('/test/project', createMockConfig());
expect(execSync).toHaveBeenCalledWith('npm run register:datadao', {
stdio: 'inherit',
cwd: '/test/project'
});
});
test.skip('handles registration errors gracefully', async () => {
// TODO: Complex integration test with error handling
inquirer.prompt.mockImplementation(({ name }) => {
if (name === 'registerDataDAO') return Promise.resolve({ registerDataDAO: true });
return Promise.resolve({ [name]: false });
});
execSync.mockImplementationOnce(() => {
throw new Error('Registration failed');
});
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('DataDAO registration failed'));
});
test.skip('shows completion status correctly', async () => {
// TODO: Complex integration test with state checking
const completeDeployment = {
state: {
contractsDeployed: true,
dataDAORegistered: true,
proofGitSetup: true,
refinerGitSetup: true,
proofConfigured: true,
refinerConfigured: true,
uiConfigured: true
}
};
fs.readFileSync.mockReturnValue(JSON.stringify(completeDeployment));
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Your DataDAO is fully configured and ready!'));
});
test.skip('validates prerequisites for proof configuration', async () => {
// TODO: Complex validation test
const mockDeployment = {
state: {
contractsDeployed: true,
proofGitSetup: false,
dataDAORegistered: false
}
};
fs.readFileSync.mockReturnValue(JSON.stringify(mockDeployment));
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GitHub setup required first'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('DataDAO registration required first'));
});
test.skip('provides UI testing instructions', async () => {
// TODO: Complex UI instruction test
inquirer.prompt.mockImplementation(({ name }) => {
if (name === 'showUIInstructions') return Promise.resolve({ showUIInstructions: true });
if (name === 'openUILater') return Promise.resolve({ openUILater: true });
return Promise.resolve({ [name]: false });
});
await guideNextSteps('/test/project', createMockConfig());
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('UI Testing Instructions'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('npm run ui:dev'));
});
});
describe('checkGitHubCLI', () => {
test('detects available and authenticated GitHub CLI', async () => {
execSync
.mockReturnValueOnce('gh version 2.0.0') // gh --version
.mockReturnValueOnce('Logged in to github.com as testuser'); // gh auth status
const result = await checkGitHubCLI();
expect(result).toEqual({ available: true, authenticated: true });
expect(execSync).toHaveBeenCalledWith('gh --version', { stdio: 'pipe' });
expect(execSync).toHaveBeenCalledWith('gh auth status', { stdio: 'pipe', encoding: 'utf8' });
});
test('detects available but not authenticated GitHub CLI', async () => {
execSync
.mockReturnValueOnce('gh version 2.0.0') // gh --version
.mockImplementationOnce(() => {
throw new Error('not authenticated');
}); // gh auth status
const result = await checkGitHubCLI();
expect(result).toEqual({ available: true, authenticated: false });
});
test('detects unavailable GitHub CLI', async () => {
execSync.mockImplementationOnce(() => {
throw new Error('command not found');
});
const result = await checkGitHubCLI();
expect(result).toEqual({ available: false, authenticated: false });
});
test('handles auth status with "not logged in" message', async () => {
execSync
.mockReturnValueOnce('gh version 2.0.0')
.mockReturnValueOnce('You are not logged in to any GitHub hosts');
const result = await checkGitHubCLI();
expect(result).toEqual({ available: true, authenticated: false });
});
test('handles auth status with "not authenticated" message', async () => {
execSync
.mockReturnValueOnce('gh version 2.0.0')
.mockReturnValueOnce('You are not authenticated to GitHub');
const result = await checkGitHubCLI();
expect(result).toEqual({ available: true, authenticated: false });
});
});
describe('createRepositoriesAutomatically', () => {
test('creates repositories successfully', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
// Mock GitHub CLI commands
execSync
.mockImplementationOnce(() => {
throw new Error('Repository not found'); // gh repo view (repo doesn't exist)
})
.mockReturnValueOnce('') // gh repo create
.mockReturnValueOnce('') // gh api (enable actions)
.mockImplementationOnce(() => {
throw new Error('Repository not found'); // gh repo view (second repo doesn't exist)
})
.mockReturnValueOnce('') // gh repo create
.mockReturnValueOnce(''); // gh api (enable actions)
const result = await createRepositoriesAutomatically(config);
expect(result.automated).toBe(true);
expect(result.proofRepo).toBe('https://github.com/testuser/test-dao-proof');
expect(result.refinerRepo).toBe('https://github.com/testuser/test-dao-refiner');
});
test('skips existing repositories', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
// Mock GitHub CLI to show repositories already exist
execSync.mockReturnValue('Repository exists');
const result = await createRepositoriesAutomatically(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('already exists, skipping'));
expect(result.automated).toBe(true);
});
test('handles repository creation errors', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
execSync
.mockImplementationOnce(() => {
throw new Error('Repository not found');
})
.mockImplementationOnce(() => {
throw new Error('API rate limit exceeded');
});
const result = await createRepositoriesAutomatically(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GitHub API rate limit reached'));
});
test('throws error for missing required configuration', async () => {
const config = createMockConfig();
delete config.dlpName;
await expect(createRepositoriesAutomatically(config)).rejects.toThrow(
'Missing required configuration for repository creation'
);
});
test('handles authentication errors', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
execSync
.mockImplementationOnce(() => {
throw new Error('Repository not found');
})
.mockImplementationOnce(() => {
throw new Error('authentication required');
});
await createRepositoriesAutomatically(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('authentication issue'));
});
test('enables GitHub Actions for created repositories', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
execSync
.mockImplementationOnce(() => {
throw new Error('Repository not found');
})
.mockReturnValueOnce('')
.mockReturnValueOnce('') // This should be the GitHub Actions enable call
.mockImplementationOnce(() => {
throw new Error('Repository not found');
})
.mockReturnValueOnce('')
.mockReturnValueOnce('');
await createRepositoriesAutomatically(config);
const actionsCalls = execSync.mock.calls.filter(call =>
call[0].includes('actions/permissions')
);
expect(actionsCalls).toHaveLength(2); // One for each repository
});
});
describe('guideManualRepositorySetup', () => {
test('provides manual setup instructions', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
inquirer.prompt.mockResolvedValue({
proofRepo: 'https://github.com/testuser/test-dao-proof',
refinerRepo: 'https://github.com/testuser/test-dao-refiner'
});
const result = await guideManualRepositorySetup(config);
expect(result.automated).toBe(false);
expect(result.proofRepo).toBe('https://github.com/testuser/test-dao-proof');
expect(result.refinerRepo).toBe('https://github.com/testuser/test-dao-refiner');
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Manual Repository Setup'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('dlp-proof-template'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('vana-data-refinement-template'));
});
test('validates GitHub URLs', async () => {
const config = createMockConfig({
dlpName: 'Test DAO',
githubUsername: 'testuser'
});
inquirer.prompt.mockImplementation((questions) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.validate) {
expect(question.validate('invalid-url')).toBe('Please enter a valid GitHub URL');
expect(question.validate('https://github.com/user/repo')).toBe(true);
}
return Promise.resolve({
[question.name]: 'https://github.com/testuser/valid-repo'
});
});
await guideManualRepositorySetup(config);
});
test('throws error for missing required configuration', async () => {
const config = createMockConfig();
delete config.githubUsername;
await expect(guideManualRepositorySetup(config)).rejects.toThrow(
'Missing required configuration for repository setup'
);
});
test('provides default repository names', async () => {
const config = createMockConfig({
dlpName: 'My Data DAO',
githubUsername: 'testuser'
});
inquirer.prompt.mockImplementation((questions) => {
const proofQuestion = Array.isArray(questions) ? questions[0] : questions;
expect(proofQuestion.default).toBe('https://github.com/testuser/my-data-dao-proof');
return Promise.resolve({
proofRepo: proofQuestion.default,
refinerRepo: 'https://github.com/testuser/my-data-dao-refiner'
});
});
await guideManualRepositorySetup(config);
});
});
describe('guideGitHubSetup', () => {
test.skip('uses automated setup when GitHub CLI is available and authenticated', async () => {
// TODO: Complex test that mocks module functions directly
const config = createMockConfig();
// Mock checkGitHubCLI to return available and authenticated
const originalCheckGitHubCLI = require('../generator').checkGitHubCLI;
require('../generator').checkGitHubCLI = jest.fn().mockResolvedValue({
available: true,
authenticated: true
});
inquirer.prompt.mockResolvedValue({ setupMethod: 'auto' });
// Mock createRepositoriesAutomatically
const originalCreateRepos = require('../generator').createRepositoriesAutomatically;
require('../generator').createRepositoriesAutomatically = jest.fn().mockResolvedValue({
proofRepo: 'https://github.com/test/repo1',
refinerRepo: 'https://github.com/test/repo2',
automated: true
});
await guideGitHubSetup(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GitHub CLI detected and authenticated'));
// Restore original functions
require('../generator').checkGitHubCLI = originalCheckGitHubCLI;
require('../generator').createRepositoriesAutomatically = originalCreateRepos;
});
test('offers authentication when GitHub CLI is available but not authenticated', async () => {
const config = createMockConfig();
const originalCheckGitHubCLI = require('../generator').checkGitHubCLI;
require('../generator').checkGitHubCLI = jest.fn()
.mockResolvedValueOnce({ available: true, authenticated: false })
.mockResolvedValueOnce({ available: true, authenticated: true });
inquirer.prompt.mockResolvedValue({ authenticateNow: true, useAutomationAfterAuth: false });
execSync.mockReturnValueOnce(''); // gh auth login
await guideGitHubSetup(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GitHub CLI detected but not authenticated'));
expect(execSync).toHaveBeenCalledWith('gh auth login', { stdio: 'inherit' });
require('../generator').checkGitHubCLI = originalCheckGitHubCLI;
});
test.skip('provides installation instructions when GitHub CLI is not available', async () => {
// TODO: Complex test with module function mocking
const config = createMockConfig();
const originalCheckGitHubCLI = require('../generator').checkGitHubCLI;
require('../generator').checkGitHubCLI = jest.fn().mockResolvedValue({
available: false,
authenticated: false
});
inquirer.prompt.mockResolvedValue({ installChoice: 'install', installCompleted: false });
await guideGitHubSetup(config);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('GitHub CLI not detected'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('https://cli.github.com/'));
require('../generator').checkGitHubCLI = originalCheckGitHubCLI;
});
test('falls back to manual setup', async () => {
const config = createMockConfig();
const originalCheckGitHubCLI = require('../generator').checkGitHubCLI;
require('../generator').checkGitHubCLI = jest.fn().mockResolvedValue({
available: false,
authenticated: false
});
const originalGuideManual = require('../generator').guideManualRepositorySetup;
require('../generator').guideManualRepositorySetup = jest.fn().mockResolvedValue({
proofRepo: 'manual-repo',
refinerRepo: 'manual-refiner',
automated: false
});
inquirer.prompt.mockResolvedValue({ installChoice: 'manual' });
const result = await guideGitHubSetup(config);
expect(result.automated).toBe(false);
require('../generator').checkGitHubCLI = originalCheckGitHubCLI;
require('../generator').guideManualRepositorySetup = originalGuideManual;
});
});
describe('integration scenarios', () => {
test('complete project generation workflow', async () => {
const targetDir = '/test/complete-project';
const config = createMockConfig();
// Mock all the sub-functions to succeed
fs.existsSync.mockReturnValue(false); // Template files exist
const result = await generateTemplate(targetDir, config);
expect(result).toBe(true);
expect(fs.ensureDirSync).toHaveBeenCalledWith(targetDir);
expect(mockTemplateEngine.processMultipleTemplates).toHaveBeenCalled();
expect(mockTemplateEngine.processTemplateToFile).toHaveBeenCalled();
expect(execSync).toHaveBeenCalledWith('npm install', expect.objectContaining({ cwd: targetDir }));
});
test('error recovery in project generation', async () => {
const targetDir = '/test/error-project';
const config = createMockConfig();
// Simulate template processing failure
mockTemplateEngine.processMultipleTemplates.mockReturnValue([
{ success: false, template: 'env/contracts.env.template', error: 'Template not found' }
]);
const result = await generateTemplate(targetDir, config);
expect(result).toBe(true); // Should still succeed
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to generate'));
});
test.skip('GitHub setup with retry logic', async () => {
// TODO: Complex retry logic test
const config = createMockConfig();
const originalCheckGitHubCLI = require('../generator').checkGitHubCLI;
require('../generator').checkGitHubCLI = jest.fn()
.mockResolvedValueOnce({ available: true, authenticated: false })
.mockResolvedValueOnce({ available: true, authenticated: false });
inquirer.prompt.mockImplementation(({ name, choices }) => {
if (name === 'authenticateNow') return Promise.resolve({ authenticateNow: false });
if (name === 'nextStep') return Promise.resolve({ nextStep: 'retry' });
if (name === 'setupMethod') return Promise.resolve({ setupMethod: 'manual' });
return Promise.resolve({});
});
// This should trigger a retry
await guideGitHubSetup(config);
expect(require('../generator').checkGitHubCLI).toHaveBeenCalledTimes(2);
require('../generator').checkGitHubCLI = originalCheckGitHubCLI;
});
});
});