@capawesome/cli
Version:
The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.
316 lines (315 loc) • 15.4 kB
JavaScript
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
import authorizationService from '../../../services/authorization-service.js';
import { findCapacitorConfigPath } from '../../../utils/capacitor-config.js';
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
import { findPackageJsonPath } from '../../../utils/package-json.js';
import userConfig from '../../../utils/user-config.js';
import consola from 'consola';
import nock from 'nock';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import createBundleCommand from './create.js';
// Mock dependencies
vi.mock('@/utils/user-config.js');
vi.mock('@/services/authorization-service.js');
vi.mock('@/utils/file.js');
vi.mock('@/utils/zip.js');
vi.mock('@/utils/buffer.js');
vi.mock('@/utils/private-key.js');
vi.mock('@/utils/hash.js');
vi.mock('@/utils/signature.js');
vi.mock('@/utils/capacitor-config.js');
vi.mock('@/utils/package-json.js');
vi.mock('consola');
describe('apps-bundles-create', () => {
const mockUserConfig = vi.mocked(userConfig);
const mockAuthorizationService = vi.mocked(authorizationService);
const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
const mockGetFilesInDirectoryAndSubdirectories = vi.mocked(getFilesInDirectoryAndSubdirectories);
const mockIsDirectory = vi.mocked(isDirectory);
const mockFindCapacitorConfigPath = vi.mocked(findCapacitorConfigPath);
const mockFindPackageJsonPath = vi.mocked(findPackageJsonPath);
const mockConsola = vi.mocked(consola);
beforeEach(() => {
vi.clearAllMocks();
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
mockFindCapacitorConfigPath.mockResolvedValue(undefined);
mockFindPackageJsonPath.mockResolvedValue(undefined);
vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`Process exited with code ${code}`);
});
});
afterEach(() => {
nock.cleanAll();
vi.restoreAllMocks();
});
it('should require authentication', async () => {
const appId = 'app-123';
const options = { appId, path: './dist', artifactType: 'zip', rollout: 1 };
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
});
it('should create bundle with self-hosted URL', async () => {
const appId = 'app-123';
const bundleUrl = 'https://example.com/bundle.zip';
const bundlePath = './bundle.zip';
const testHash = 'test-hash';
const bundleId = 'bundle-456';
const testToken = 'test-token';
const testBuffer = Buffer.from('test');
const options = {
appId,
url: bundleUrl,
path: bundlePath,
artifactType: 'zip',
rollout: 1,
};
mockFileExistsAtPath.mockResolvedValue(true);
mockIsDirectory.mockResolvedValue(false);
// Mock utility functions
const mockZip = await import('../../../utils/zip.js');
const mockBuffer = await import('../../../utils/buffer.js');
const mockHash = await import('../../../utils/hash.js');
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
const appScope = nock(DEFAULT_API_BASE_URL)
.get(`/v1/apps/${appId}`)
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(200, { id: appId, name: 'Test App' });
const bundleScope = nock(DEFAULT_API_BASE_URL)
.post(`/v1/apps/${appId}/bundles`, {
appId,
url: bundleUrl,
checksum: testHash,
artifactType: 'zip',
rolloutPercentage: 1,
})
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(201, { id: bundleId });
await createBundleCommand.action(options, undefined);
expect(appScope.isDone()).toBe(true);
expect(bundleScope.isDone()).toBe(true);
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
expect(mockConsola.info).toHaveBeenCalledWith(`Bundle ID: ${bundleId}`);
});
it('should handle path validation errors', async () => {
const appId = 'app-123';
const nonexistentPath = './nonexistent';
const options = { appId, path: nonexistentPath, artifactType: 'zip', rollout: 1 };
mockFileExistsAtPath.mockResolvedValue(false);
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist.');
});
it('should validate manifest artifact type requires directory', async () => {
const appId = 'app-123';
const bundlePath = './bundle.zip';
const options = {
appId,
path: bundlePath,
artifactType: 'manifest',
rollout: 1,
};
mockFileExistsAtPath.mockResolvedValue(true);
mockIsDirectory.mockResolvedValue(false);
// Mock zip utility to return true so path validation passes
const mockZip = await import('../../../utils/zip.js');
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
});
it('should validate manifest artifact type cannot use URL', async () => {
const appId = 'app-123';
const bundleUrl = 'https://example.com/bundle.zip';
const options = {
appId,
url: bundleUrl,
artifactType: 'manifest',
rollout: 1,
};
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('It is not yet possible to provide a URL when creating a bundle with an artifact type of `manifest`.');
});
it('should handle API error during creation', async () => {
const appId = 'app-123';
const bundleUrl = 'https://example.com/bundle.zip';
const testToken = 'test-token';
const options = {
appId,
url: bundleUrl,
artifactType: 'zip',
rollout: 1,
};
const appScope = nock(DEFAULT_API_BASE_URL)
.get(`/v1/apps/${appId}`)
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(200, { id: appId, name: 'Test App' });
const bundleScope = nock(DEFAULT_API_BASE_URL)
.post(`/v1/apps/${appId}/bundles`)
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(400, { message: 'Invalid bundle data' });
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow();
expect(appScope.isDone()).toBe(true);
expect(bundleScope.isDone()).toBe(true);
});
it('should handle private key file path', async () => {
const appId = 'app-123';
const bundleUrl = 'https://example.com/bundle.zip';
const bundlePath = './bundle.zip';
const privateKeyPath = 'private-key.pem';
const testHash = 'test-hash';
const testSignature = 'test-signature';
const bundleId = 'bundle-456';
const testToken = 'test-token';
const testBuffer = Buffer.from('test');
const options = {
appId,
url: bundleUrl,
path: bundlePath,
privateKey: privateKeyPath,
artifactType: 'zip',
rollout: 1,
};
mockFileExistsAtPath.mockImplementation((path) => {
if (path === privateKeyPath)
return Promise.resolve(true);
if (path === bundlePath)
return Promise.resolve(true);
return Promise.resolve(false);
});
mockIsDirectory.mockResolvedValue(false);
// Mock utility functions
const mockZip = await import('../../../utils/zip.js');
const mockBuffer = await import('../../../utils/buffer.js');
const mockPrivateKey = await import('../../../utils/private-key.js');
const mockHash = await import('../../../utils/hash.js');
const mockSignature = await import('../../../utils/signature.js');
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
const appScope = nock(DEFAULT_API_BASE_URL)
.get(`/v1/apps/${appId}`)
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(200, { id: appId, name: 'Test App' });
const bundleScope = nock(DEFAULT_API_BASE_URL)
.post(`/v1/apps/${appId}/bundles`, {
appId,
url: bundleUrl,
checksum: testHash,
signature: testSignature,
artifactType: 'zip',
rolloutPercentage: 1,
})
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(201, { id: bundleId });
await createBundleCommand.action(options, undefined);
expect(appScope.isDone()).toBe(true);
expect(bundleScope.isDone()).toBe(true);
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
});
it('should handle private key plain text content', async () => {
const appId = 'app-123';
const bundleUrl = 'https://example.com/bundle.zip';
const bundlePath = './bundle.zip';
const privateKeyContent = '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCgxvzJrMCbmtjb\n-----END PRIVATE KEY-----';
const testHash = 'test-hash';
const testSignature = 'test-signature';
const bundleId = 'bundle-456';
const testToken = 'test-token';
const testBuffer = Buffer.from('test');
const options = {
appId,
url: bundleUrl,
path: bundlePath,
privateKey: privateKeyContent,
artifactType: 'zip',
rollout: 1,
};
mockFileExistsAtPath.mockResolvedValue(true);
mockIsDirectory.mockResolvedValue(false);
// Mock utility functions
const mockZip = await import('../../../utils/zip.js');
const mockBuffer = await import('../../../utils/buffer.js');
const mockPrivateKey = await import('../../../utils/private-key.js');
const mockHash = await import('../../../utils/hash.js');
const mockSignature = await import('../../../utils/signature.js');
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(true);
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
const appScope = nock(DEFAULT_API_BASE_URL)
.get(`/v1/apps/${appId}`)
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(200, { id: appId, name: 'Test App' });
const bundleScope = nock(DEFAULT_API_BASE_URL)
.post(`/v1/apps/${appId}/bundles`, {
appId,
url: bundleUrl,
checksum: testHash,
signature: testSignature,
artifactType: 'zip',
rolloutPercentage: 1,
})
.matchHeader('Authorization', `Bearer ${testToken}`)
.reply(201, { id: bundleId });
await createBundleCommand.action(options, undefined);
expect(appScope.isDone()).toBe(true);
expect(bundleScope.isDone()).toBe(true);
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
});
it('should handle private key file not found', async () => {
const appId = 'app-123';
const privateKeyPath = 'nonexistent-key.pem';
const options = {
appId,
path: './dist',
privateKey: privateKeyPath,
artifactType: 'zip',
rollout: 1,
};
mockFileExistsAtPath.mockImplementation((path) => {
if (path === privateKeyPath)
return Promise.resolve(false);
return Promise.resolve(true);
});
mockIsDirectory.mockResolvedValue(true);
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
]);
// Mock utility functions
const mockBuffer = await import('../../../utils/buffer.js');
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('Private key file not found.');
});
it('should handle invalid private key format', async () => {
const appId = 'app-123';
const invalidPrivateKey = 'not-a-valid-key';
const options = {
appId,
path: './dist',
privateKey: invalidPrivateKey,
artifactType: 'zip',
rollout: 1,
};
mockFileExistsAtPath.mockResolvedValue(true);
mockIsDirectory.mockResolvedValue(false);
// Mock zip utility to pass path validation
const mockZip = await import('../../../utils/zip.js');
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
// Mock utility functions
const mockBuffer = await import('../../../utils/buffer.js');
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
expect(mockConsola.error).toHaveBeenCalledWith('Private key must be either a path to a .pem file or the private key content as plain text.');
});
});