refactor: Move community package logic to service (no-changelog) (#6973)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Db from '@/Db';
|
||||
import { audit } from '@/audit';
|
||||
import * as packageModel from '@/CommunityNodes/packageModel';
|
||||
import { OFFICIAL_RISKY_NODE_TYPES, NODES_REPORT } from '@/audit/constants';
|
||||
import { getRiskSection, MOCK_PACKAGE, saveManualTriggerWorkflow } from './utils';
|
||||
import * as testDb from '../shared/testDb';
|
||||
@@ -9,10 +8,14 @@ import { toReportTitle } from '@/audit/utils';
|
||||
import { mockInstance } from '../shared/utils/';
|
||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
||||
import Container from 'typedi';
|
||||
|
||||
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
||||
nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
|
||||
mockInstance(NodeTypes);
|
||||
const communityPackageService = mockInstance(CommunityPackageService);
|
||||
Container.set(CommunityPackageService, communityPackageService);
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
@@ -24,9 +27,11 @@ beforeEach(async () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should report risky official nodes', async () => {
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||
const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => {
|
||||
return (acc[cur] = uuid()), acc;
|
||||
}, {});
|
||||
@@ -71,6 +76,7 @@ test('should report risky official nodes', async () => {
|
||||
});
|
||||
|
||||
test('should not report non-risky official nodes', async () => {
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||
await saveManualTriggerWorkflow();
|
||||
|
||||
const testAudit = await audit(['nodes']);
|
||||
@@ -85,7 +91,7 @@ test('should not report non-risky official nodes', async () => {
|
||||
});
|
||||
|
||||
test('should report community nodes', async () => {
|
||||
jest.spyOn(packageModel, 'getAllInstalledPackages').mockResolvedValueOnce(MOCK_PACKAGE);
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||
|
||||
const testAudit = await audit(['nodes']);
|
||||
|
||||
|
||||
@@ -1,106 +1,104 @@
|
||||
import path from 'path';
|
||||
import { mocked } from 'jest-mock';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
import {
|
||||
executeCommand,
|
||||
checkNpmPackageStatus,
|
||||
hasPackageLoaded,
|
||||
removePackageFromMissingList,
|
||||
isNpmError,
|
||||
} from '@/CommunityNodes/helpers';
|
||||
import { findInstalledPackage, isPackageInstalled } from '@/CommunityNodes/packageModel';
|
||||
|
||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import { Push } from '@/push';
|
||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
||||
|
||||
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
|
||||
import * as utils from './shared/utils/';
|
||||
import * as testDb from './shared/testDb';
|
||||
import {
|
||||
mockInstance,
|
||||
setupTestServer,
|
||||
mockPackage,
|
||||
mockNode,
|
||||
mockPackageName,
|
||||
} from './shared/utils';
|
||||
|
||||
const mockLoadNodesAndCredentials = utils.mockInstance(LoadNodesAndCredentials);
|
||||
utils.mockInstance(NodeTypes);
|
||||
utils.mockInstance(Push);
|
||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
|
||||
jest.mock('@/CommunityNodes/helpers', () => {
|
||||
return {
|
||||
...jest.requireActual('@/CommunityNodes/helpers'),
|
||||
checkNpmPackageStatus: jest.fn(),
|
||||
executeCommand: jest.fn(),
|
||||
hasPackageLoaded: jest.fn(),
|
||||
isNpmError: jest.fn(),
|
||||
removePackageFromMissingList: jest.fn(),
|
||||
};
|
||||
});
|
||||
const communityPackageService = mockInstance(CommunityPackageService);
|
||||
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
||||
mockInstance(Push);
|
||||
|
||||
jest.mock('@/CommunityNodes/packageModel', () => {
|
||||
return {
|
||||
...jest.requireActual('@/CommunityNodes/packageModel'),
|
||||
isPackageInstalled: jest.fn(),
|
||||
findInstalledPackage: jest.fn(),
|
||||
};
|
||||
});
|
||||
const testServer = setupTestServer({ endpointGroups: ['nodes'] });
|
||||
|
||||
const mockedEmptyPackage = mocked(utils.emptyPackage);
|
||||
const commonUpdatesProps = {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
installedVersion: COMMUNITY_PACKAGE_VERSION.CURRENT,
|
||||
updateAvailable: COMMUNITY_PACKAGE_VERSION.UPDATED,
|
||||
};
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['nodes'] });
|
||||
const parsedNpmPackageName = {
|
||||
packageName: 'test',
|
||||
rawString: 'test',
|
||||
};
|
||||
|
||||
let ownerShell: User;
|
||||
let authOwnerShellAgent: SuperAgentTest;
|
||||
let authAgent: SuperAgentTest;
|
||||
|
||||
beforeAll(async () => {
|
||||
const globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||
ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
|
||||
const ownerShell = await testDb.createOwner();
|
||||
authAgent = testServer.authAgentFor(ownerShell);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['InstalledNodes', 'InstalledPackages']);
|
||||
|
||||
mocked(executeCommand).mockReset();
|
||||
mocked(findInstalledPackage).mockReset();
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /nodes', () => {
|
||||
test('should respond 200 if no nodes are installed', async () => {
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([]);
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authOwnerShellAgent.get('/nodes');
|
||||
} = await authAgent.get('/nodes').expect(200);
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return list of one installed package and node', async () => {
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
const pkg = mockPackage();
|
||||
const node = mockNode(pkg.packageName);
|
||||
pkg.installedNodes = [node];
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authOwnerShellAgent.get('/nodes');
|
||||
} = await authAgent.get('/nodes').expect(200);
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0].installedNodes).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should return list of multiple installed packages and nodes', async () => {
|
||||
const first = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName));
|
||||
const pkgA = mockPackage();
|
||||
const nodeA = mockNode(pkgA.packageName);
|
||||
|
||||
const second = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
|
||||
const pkgB = mockPackage();
|
||||
const nodeB = mockNode(pkgB.packageName);
|
||||
const nodeC = mockNode(pkgB.packageName);
|
||||
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
|
||||
|
||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
|
||||
{
|
||||
...commonUpdatesProps,
|
||||
packageName: pkgA.packageName,
|
||||
installedNodes: [nodeA],
|
||||
},
|
||||
{
|
||||
...commonUpdatesProps,
|
||||
packageName: pkgB.packageName,
|
||||
installedNodes: [nodeB, nodeC],
|
||||
},
|
||||
]);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authOwnerShellAgent.get('/nodes');
|
||||
} = await authAgent.get('/nodes').expect(200);
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(2);
|
||||
|
||||
const allNodes = data.reduce(
|
||||
@@ -112,166 +110,141 @@ describe('GET /nodes', () => {
|
||||
});
|
||||
|
||||
test('should not check for updates if no packages installed', async () => {
|
||||
await authOwnerShellAgent.get('/nodes');
|
||||
await authAgent.get('/nodes');
|
||||
|
||||
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
|
||||
expect(communityPackageService.executeNpmCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should check for updates if packages installed', async () => {
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
|
||||
|
||||
await authOwnerShellAgent.get('/nodes');
|
||||
await authAgent.get('/nodes').expect(200);
|
||||
|
||||
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
|
||||
doNotHandleError: true,
|
||||
});
|
||||
const args = ['npm outdated --json', { doNotHandleError: true }];
|
||||
|
||||
expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args);
|
||||
});
|
||||
|
||||
test('should report package updates if available', async () => {
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
const pkg = mockPackage();
|
||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
||||
|
||||
mocked(executeCommand).mockImplementationOnce(() => {
|
||||
communityPackageService.executeNpmCommand.mockImplementation(() => {
|
||||
throw {
|
||||
code: 1,
|
||||
stdout: JSON.stringify({
|
||||
[packageName]: {
|
||||
[pkg.packageName]: {
|
||||
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
|
||||
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
|
||||
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
|
||||
location: path.join('node_modules', packageName),
|
||||
location: path.join('node_modules', pkg.packageName),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
mocked(isNpmError).mockReturnValueOnce(true);
|
||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
|
||||
{
|
||||
packageName: 'test',
|
||||
installedNodes: [],
|
||||
...commonUpdatesProps,
|
||||
},
|
||||
]);
|
||||
|
||||
const {
|
||||
body: { data },
|
||||
} = await authOwnerShellAgent.get('/nodes');
|
||||
} = await authAgent.get('/nodes').expect(200);
|
||||
|
||||
expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT);
|
||||
expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED);
|
||||
const [returnedPkg] = data;
|
||||
|
||||
expect(returnedPkg.installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT);
|
||||
expect(returnedPkg.updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /nodes', () => {
|
||||
test('should reject if package name is missing', async () => {
|
||||
const { statusCode } = await authOwnerShellAgent.post('/nodes');
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
await authAgent.post('/nodes').expect(400);
|
||||
});
|
||||
|
||||
test('should reject if package is duplicate', async () => {
|
||||
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
|
||||
mocked(isPackageInstalled).mockResolvedValueOnce(true);
|
||||
mocked(hasPackageLoaded).mockReturnValueOnce(true);
|
||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||
communityPackageService.isPackageInstalled.mockResolvedValue(true);
|
||||
communityPackageService.hasPackageLoaded.mockReturnValue(true);
|
||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authOwnerShellAgent.post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('already installed');
|
||||
});
|
||||
|
||||
test('should allow installing packages that could not be loaded', async () => {
|
||||
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
|
||||
mocked(hasPackageLoaded).mockReturnValueOnce(false);
|
||||
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
|
||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||
communityPackageService.hasPackageLoaded.mockReturnValue(false);
|
||||
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
|
||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||
mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage());
|
||||
|
||||
mockLoadNodesAndCredentials.installNpmModule.mockImplementationOnce(mockedEmptyPackage);
|
||||
await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(200);
|
||||
|
||||
const { statusCode } = await authOwnerShellAgent.post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
|
||||
expect(communityPackageService.removePackageFromMissingList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not install a banned package', async () => {
|
||||
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
|
||||
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
|
||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authOwnerShellAgent.post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('banned');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /nodes', () => {
|
||||
test('should not delete if package name is empty', async () => {
|
||||
const response = await authOwnerShellAgent.delete('/nodes');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
await authAgent.delete('/nodes').expect(400);
|
||||
});
|
||||
|
||||
test('should reject if package is not installed', async () => {
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authOwnerShellAgent.delete('/nodes').query({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
} = await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(400);
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('not installed');
|
||||
});
|
||||
|
||||
test('should uninstall package', async () => {
|
||||
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
|
||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||
|
||||
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
|
||||
await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(200);
|
||||
|
||||
const { statusCode } = await authOwnerShellAgent.delete('/nodes').query({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(removeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadNodesAndCredentials.removeNpmModule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /nodes', () => {
|
||||
test('should reject if package name is empty', async () => {
|
||||
const response = await authOwnerShellAgent.patch('/nodes');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
await authAgent.patch('/nodes').expect(400);
|
||||
});
|
||||
|
||||
test('reject if package is not installed', async () => {
|
||||
test('should reject if package is not installed', async () => {
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authOwnerShellAgent.patch('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
} = await authAgent.patch('/nodes').send({ name: mockPackageName() }).expect(400);
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('not installed');
|
||||
});
|
||||
|
||||
test('should update a package', async () => {
|
||||
const updateSpy =
|
||||
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
|
||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||
|
||||
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
|
||||
await authAgent.patch('/nodes').send({ name: mockPackageName() });
|
||||
|
||||
await authOwnerShellAgent.patch('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadNodesAndCredentials.updateNpmModule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ import { sqliteMigrations } from '@db/migrations/sqlite';
|
||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import type { Role } from '@db/entities/Role';
|
||||
import type { TagEntity } from '@db/entities/TagEntity';
|
||||
import type { User } from '@db/entities/User';
|
||||
@@ -23,13 +21,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { ICredentialsDb } from '@/Interfaces';
|
||||
import { DB_INITIALIZATION_TIMEOUT } from './constants';
|
||||
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
|
||||
import type {
|
||||
CollectionName,
|
||||
CredentialPayload,
|
||||
InstalledNodePayload,
|
||||
InstalledPackagePayload,
|
||||
PostgresSchemaSection,
|
||||
} from './types';
|
||||
import type { CollectionName, CredentialPayload, PostgresSchemaSection } from './types';
|
||||
import type { ExecutionData } from '@db/entities/ExecutionData';
|
||||
import { generateNanoId } from '@db/utils/generators';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
@@ -292,31 +284,6 @@ export async function createManyUsers(
|
||||
return Db.collections.User.save(users);
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// Installed nodes and packages creation
|
||||
// --------------------------------------
|
||||
|
||||
export async function saveInstalledPackage(
|
||||
installedPackagePayload: InstalledPackagePayload,
|
||||
): Promise<InstalledPackages> {
|
||||
const newInstalledPackage = new InstalledPackages();
|
||||
|
||||
Object.assign(newInstalledPackage, installedPackagePayload);
|
||||
|
||||
const savedInstalledPackage = await Db.collections.InstalledPackages.save(newInstalledPackage);
|
||||
return savedInstalledPackage;
|
||||
}
|
||||
|
||||
export async function saveInstalledNode(
|
||||
installedNodePayload: InstalledNodePayload,
|
||||
): Promise<InstalledNodes> {
|
||||
const newInstalledNode = new InstalledNodes();
|
||||
|
||||
Object.assign(newInstalledNode, installedNodePayload);
|
||||
|
||||
return Db.collections.InstalledNodes.save(newInstalledNode);
|
||||
}
|
||||
|
||||
export async function addApiKey(user: User): Promise<User> {
|
||||
user.apiKey = randomApiKey();
|
||||
return Db.collections.User.save(user);
|
||||
|
||||
@@ -59,15 +59,3 @@ export type SaveCredentialFunction = (
|
||||
export type PostgresSchemaSection = {
|
||||
[K in 'host' | 'port' | 'schema' | 'user' | 'password']: { env: string };
|
||||
};
|
||||
|
||||
export type InstalledPackagePayload = {
|
||||
packageName: string;
|
||||
installedVersion: string;
|
||||
};
|
||||
|
||||
export type InstalledNodePayload = {
|
||||
name: string;
|
||||
type: string;
|
||||
latestVersion: number;
|
||||
package: string;
|
||||
};
|
||||
|
||||
@@ -3,27 +3,44 @@ import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
|
||||
import { randomName } from '../random';
|
||||
import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '../constants';
|
||||
import type { InstalledNodePayload, InstalledPackagePayload } from '../types';
|
||||
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
|
||||
import Container from 'typedi';
|
||||
|
||||
export function installedPackagePayload(): InstalledPackagePayload {
|
||||
return {
|
||||
packageName: NODE_PACKAGE_PREFIX + randomName(),
|
||||
export const mockPackageName = () => NODE_PACKAGE_PREFIX + randomName();
|
||||
|
||||
export const mockPackage = () =>
|
||||
Container.get(InstalledPackagesRepository).create({
|
||||
packageName: mockPackageName(),
|
||||
installedVersion: COMMUNITY_PACKAGE_VERSION.CURRENT,
|
||||
};
|
||||
}
|
||||
installedNodes: [],
|
||||
});
|
||||
|
||||
export function installedNodePayload(packageName: string): InstalledNodePayload {
|
||||
export const mockNode = (packageName: string) => {
|
||||
const nodeName = randomName();
|
||||
return {
|
||||
|
||||
return Container.get(InstalledNodesRepository).create({
|
||||
name: nodeName,
|
||||
type: nodeName,
|
||||
latestVersion: COMMUNITY_NODE_VERSION.CURRENT,
|
||||
package: packageName,
|
||||
};
|
||||
}
|
||||
latestVersion: COMMUNITY_NODE_VERSION.CURRENT.toString(),
|
||||
package: { packageName },
|
||||
});
|
||||
};
|
||||
|
||||
export const emptyPackage = async () => {
|
||||
const installedPackage = new InstalledPackages();
|
||||
installedPackage.installedNodes = [];
|
||||
return installedPackage;
|
||||
};
|
||||
|
||||
export function mockPackagePair(): InstalledPackages[] {
|
||||
const pkgA = mockPackage();
|
||||
const nodeA = mockNode(pkgA.packageName);
|
||||
pkgA.installedNodes = [nodeA];
|
||||
|
||||
const pkgB = mockPackage();
|
||||
const nodeB1 = mockNode(pkgB.packageName);
|
||||
const nodeB2 = mockNode(pkgB.packageName);
|
||||
pkgB.installedNodes = [nodeB1, nodeB2];
|
||||
|
||||
return [pkgA, pkgB];
|
||||
}
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
checkNpmPackageStatus,
|
||||
matchPackagesWithUpdates,
|
||||
executeCommand,
|
||||
parseNpmPackageName,
|
||||
matchMissingPackages,
|
||||
hasPackageLoaded,
|
||||
removePackageFromMissingList,
|
||||
} from '@/CommunityNodes/helpers';
|
||||
import {
|
||||
NODE_PACKAGE_PREFIX,
|
||||
NPM_COMMAND_TOKENS,
|
||||
NPM_PACKAGE_STATUS_GOOD,
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
} from '@/constants';
|
||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||
import { randomName } from '../integration/shared/random';
|
||||
import config from '@/config';
|
||||
import { installedPackagePayload, installedNodePayload } from '../integration/shared/utils/';
|
||||
|
||||
import type { CommunityPackages } from '@/Interfaces';
|
||||
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('child_process');
|
||||
jest.mock('axios');
|
||||
|
||||
describe('parsePackageName', () => {
|
||||
test('Should fail with empty package name', () => {
|
||||
expect(() => parseNpmPackageName('')).toThrowError();
|
||||
});
|
||||
|
||||
test('Should fail with invalid package prefix name', () => {
|
||||
expect(() => parseNpmPackageName('INVALID_PREFIX@123')).toThrowError();
|
||||
});
|
||||
|
||||
test('Should parse valid package name', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const parsed = parseNpmPackageName(validPackageName);
|
||||
|
||||
expect(parsed.rawString).toBe(validPackageName);
|
||||
expect(parsed.packageName).toBe(validPackageName);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should parse valid package name and version', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageName}@${validPackageVersion}`;
|
||||
const parsed = parseNpmPackageName(fullPackageName);
|
||||
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(validPackageName);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBe(validPackageVersion);
|
||||
});
|
||||
|
||||
test('Should parse valid package name, scope and version', () => {
|
||||
const validPackageScope = '@n8n';
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageScope}/${validPackageName}@${validPackageVersion}`;
|
||||
const parsed = parseNpmPackageName(fullPackageName);
|
||||
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(`${validPackageScope}/${validPackageName}`);
|
||||
expect(parsed.scope).toBe(validPackageScope);
|
||||
expect(parsed.version).toBe(validPackageVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeCommand', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fsAccess.mockReset();
|
||||
// @ts-ignore
|
||||
fsMkdir.mockReset();
|
||||
// @ts-ignore
|
||||
exec.mockReset();
|
||||
});
|
||||
|
||||
test('Should call command with valid options', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
expect(args[1].cwd).toBeDefined();
|
||||
expect(args[1].env).toBeDefined();
|
||||
// PATH or NODE_PATH may be undefined depending on environment so we don't check for these keys.
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('Should make sure folder exists', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('Should try to create folder if it does not exist', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
fsAccess.mockImplementation(() => {
|
||||
throw new Error('Folder does not exist.');
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should throw especial error when package is not found', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(
|
||||
new Error(
|
||||
'Something went wrong - ' +
|
||||
NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR +
|
||||
'. Aborting.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
await expect(async () => executeCommand('ls')).rejects.toThrow(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND,
|
||||
);
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crossInformationPackage', () => {
|
||||
test('Should return same list if availableUpdates is undefined', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages);
|
||||
expect(crossedData).toEqual(fakePackages);
|
||||
});
|
||||
|
||||
test('Should correctly match update versions for packages', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[fakePackages[0].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.2.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBe('0.2.0');
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
|
||||
test('Should correctly match update versions for single package', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBeUndefined();
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchMissingPackages', () => {
|
||||
test('Should not match failed packages that do not exist', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages).toEqual(fakePackages);
|
||||
expect(matchedPackages[0].failedLoading).toBeUndefined();
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should match failed packages that should be present', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@${fakePackages[0].installedVersion}`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should match failed packages even if version is wrong', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@123.456.789`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkNpmPackageStatus', () => {
|
||||
test('Should call axios.post', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
await checkNpmPackageStatus(packageName);
|
||||
expect(axios.post).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should not fail if request fails', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
axios.post = jest.fn(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
const result = await checkNpmPackageStatus(packageName);
|
||||
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
||||
});
|
||||
|
||||
test('Should warn if package is banned', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
// @ts-ignore
|
||||
axios.post = jest.fn(() => {
|
||||
return { data: { status: 'Banned', reason: 'Not good' } };
|
||||
});
|
||||
const result = await checkNpmPackageStatus(packageName);
|
||||
expect(result.status).toBe('Banned');
|
||||
expect(result.reason).toBe('Not good');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPackageLoadedSuccessfully', () => {
|
||||
test('Should return true when failed package list does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
const result = hasPackageLoaded('package');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return true when package is not in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoaded('packageC');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return false when package is in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoaded('packageA');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePackageFromMissingList', () => {
|
||||
test('Should do nothing if key does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
|
||||
removePackageFromMissingList('packageA');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should remove only correct package from list', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0');
|
||||
|
||||
removePackageFromMissingList('packageB');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe('packageA@0.1.0 packageBB@0.2.0');
|
||||
});
|
||||
|
||||
test('Should not remove if package is not in the list', () => {
|
||||
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0';
|
||||
config.set('nodes.packagesMissing', failedToLoadList);
|
||||
|
||||
removePackageFromMissingList('packageC');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe(failedToLoadList);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate a list with 2 packages, one with a single node and another with 2 nodes
|
||||
*/
|
||||
function generateListOfFakeInstalledPackages(): InstalledPackages[] {
|
||||
const fakeInstalledPackage1 = new InstalledPackages();
|
||||
Object.assign(fakeInstalledPackage1, installedPackagePayload());
|
||||
|
||||
const fakeInstalledNode1 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode1, installedNodePayload(fakeInstalledPackage1.packageName));
|
||||
|
||||
fakeInstalledPackage1.installedNodes = [fakeInstalledNode1];
|
||||
|
||||
const fakeInstalledPackage2 = new InstalledPackages();
|
||||
Object.assign(fakeInstalledPackage2, installedPackagePayload());
|
||||
|
||||
const fakeInstalledNode2 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode2, installedNodePayload(fakeInstalledPackage2.packageName));
|
||||
|
||||
const fakeInstalledNode3 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode3, installedNodePayload(fakeInstalledPackage2.packageName));
|
||||
|
||||
fakeInstalledPackage2.installedNodes = [fakeInstalledNode2, fakeInstalledNode3];
|
||||
|
||||
return [fakeInstalledPackage1, fakeInstalledPackage2];
|
||||
}
|
||||
357
packages/cli/test/unit/services/communityPackage.service.test.ts
Normal file
357
packages/cli/test/unit/services/communityPackage.service.test.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { exec } from 'child_process';
|
||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
NODE_PACKAGE_PREFIX,
|
||||
NPM_COMMAND_TOKENS,
|
||||
NPM_PACKAGE_STATUS_GOOD,
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
} from '@/constants';
|
||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import { randomName } from '../../integration/shared/random';
|
||||
import config from '@/config';
|
||||
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import type { CommunityPackages } from '@/Interfaces';
|
||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
||||
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
|
||||
import Container from 'typedi';
|
||||
import { InstalledNodes } from '@/databases/entities/InstalledNodes';
|
||||
import {
|
||||
COMMUNITY_NODE_VERSION,
|
||||
COMMUNITY_PACKAGE_VERSION,
|
||||
} from '../../integration/shared/constants';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('child_process');
|
||||
jest.mock('axios');
|
||||
|
||||
type ExecOptions = NonNullable<Parameters<typeof exec>[1]>;
|
||||
type ExecCallback = NonNullable<Parameters<typeof exec>[2]>;
|
||||
|
||||
const execMock = ((...args) => {
|
||||
const cb = args[args.length - 1] as ExecCallback;
|
||||
cb(null, 'Done', '');
|
||||
}) as typeof exec;
|
||||
|
||||
describe('CommunityPackageService', () => {
|
||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||
Container.set(InstalledNodesRepository, installedNodesRepository);
|
||||
|
||||
installedNodesRepository.create.mockImplementation(() => {
|
||||
const nodeName = randomName();
|
||||
|
||||
return Object.assign(new InstalledNodes(), {
|
||||
name: nodeName,
|
||||
type: nodeName,
|
||||
latestVersion: COMMUNITY_NODE_VERSION.CURRENT.toString(),
|
||||
packageName: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
|
||||
|
||||
installedPackageRepository.create.mockImplementation(() => {
|
||||
return Object.assign(new InstalledPackages(), {
|
||||
packageName: mockPackageName(),
|
||||
installedVersion: COMMUNITY_PACKAGE_VERSION.CURRENT,
|
||||
});
|
||||
});
|
||||
|
||||
const communityPackageService = new CommunityPackageService(installedPackageRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
config.load(config.default);
|
||||
});
|
||||
|
||||
describe('parseNpmPackageName()', () => {
|
||||
test('should fail with empty package name', () => {
|
||||
expect(() => communityPackageService.parseNpmPackageName('')).toThrowError();
|
||||
});
|
||||
|
||||
test('should fail with invalid package prefix name', () => {
|
||||
expect(() =>
|
||||
communityPackageService.parseNpmPackageName('INVALID_PREFIX@123'),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('should parse valid package name', () => {
|
||||
const name = mockPackageName();
|
||||
const parsed = communityPackageService.parseNpmPackageName(name);
|
||||
|
||||
expect(parsed.rawString).toBe(name);
|
||||
expect(parsed.packageName).toBe(name);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should parse valid package name and version', () => {
|
||||
const name = mockPackageName();
|
||||
const version = '0.1.1';
|
||||
const fullPackageName = `${name}@${version}`;
|
||||
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
|
||||
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(name);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBe(version);
|
||||
});
|
||||
|
||||
test('should parse valid package name, scope and version', () => {
|
||||
const scope = '@n8n';
|
||||
const name = mockPackageName();
|
||||
const version = '0.1.1';
|
||||
const fullPackageName = `${scope}/${name}@${version}`;
|
||||
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
|
||||
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(`${scope}/${name}`);
|
||||
expect(parsed.scope).toBe(scope);
|
||||
expect(parsed.version).toBe(version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeCommand()', () => {
|
||||
beforeEach(() => {
|
||||
mocked(fsAccess).mockReset();
|
||||
mocked(fsMkdir).mockReset();
|
||||
mocked(exec).mockReset();
|
||||
});
|
||||
|
||||
test('should call command with valid options', async () => {
|
||||
const execMock = ((...args) => {
|
||||
const arg = args[1] as ExecOptions;
|
||||
expect(arg.cwd).toBeDefined();
|
||||
expect(arg.env).toBeDefined();
|
||||
// PATH or NODE_PATH may be undefined depending on environment so we don't check for these keys.
|
||||
const cb = args[args.length - 1] as ExecCallback;
|
||||
cb(null, 'Done', '');
|
||||
}) as typeof exec;
|
||||
|
||||
mocked(exec).mockImplementation(execMock);
|
||||
|
||||
await communityPackageService.executeNpmCommand('ls');
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should make sure folder exists', async () => {
|
||||
mocked(exec).mockImplementation(execMock);
|
||||
|
||||
await communityPackageService.executeNpmCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should try to create folder if it does not exist', async () => {
|
||||
mocked(exec).mockImplementation(execMock);
|
||||
mocked(fsAccess).mockImplementation(() => {
|
||||
throw new Error('Folder does not exist.');
|
||||
});
|
||||
|
||||
await communityPackageService.executeNpmCommand('ls');
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw especial error when package is not found', async () => {
|
||||
const erroringExecMock = ((...args) => {
|
||||
const cb = args[args.length - 1] as ExecCallback;
|
||||
const msg = `Something went wrong - ${NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR}. Aborting.`;
|
||||
cb(new Error(msg), '', '');
|
||||
}) as typeof exec;
|
||||
|
||||
mocked(exec).mockImplementation(erroringExecMock);
|
||||
|
||||
const call = async () => communityPackageService.executeNpmCommand('ls');
|
||||
|
||||
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crossInformationPackage()', () => {
|
||||
test('should return same list if availableUpdates is undefined', () => {
|
||||
const fakePkgs = mockPackagePair();
|
||||
|
||||
const crossedPkgs = communityPackageService.matchPackagesWithUpdates(fakePkgs);
|
||||
|
||||
expect(crossedPkgs).toEqual(fakePkgs);
|
||||
});
|
||||
|
||||
test('should correctly match update versions for packages', () => {
|
||||
const [pkgA, pkgB] = mockPackagePair();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[pkgA.packageName]: {
|
||||
current: pkgA.installedVersion,
|
||||
wanted: pkgA.installedVersion,
|
||||
latest: '0.2.0',
|
||||
location: pkgA.packageName,
|
||||
},
|
||||
[pkgB.packageName]: {
|
||||
current: pkgA.installedVersion,
|
||||
wanted: pkgA.installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: pkgA.packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
||||
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
||||
|
||||
expect(crossedPkgA.updateAvailable).toBe('0.2.0');
|
||||
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
|
||||
test('should correctly match update versions for single package', () => {
|
||||
const [pkgA, pkgB] = mockPackagePair();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[pkgB.packageName]: {
|
||||
current: pkgA.installedVersion,
|
||||
wanted: pkgA.installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: pkgA.packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
||||
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
||||
|
||||
expect(crossedPkgA.updateAvailable).toBeUndefined();
|
||||
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchMissingPackages()', () => {
|
||||
test('should not match failed packages that do not exist', () => {
|
||||
const fakePkgs = mockPackagePair();
|
||||
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`;
|
||||
|
||||
const matchedPackages = communityPackageService.matchMissingPackages(
|
||||
fakePkgs,
|
||||
notFoundPkgNames,
|
||||
);
|
||||
|
||||
expect(matchedPackages).toEqual(fakePkgs);
|
||||
|
||||
const [first, second] = matchedPackages;
|
||||
|
||||
expect(first.failedLoading).toBeUndefined();
|
||||
expect(second.failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should match failed packages that should be present', () => {
|
||||
const [pkgA, pkgB] = mockPackagePair();
|
||||
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@${pkgA.installedVersion}`;
|
||||
|
||||
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
|
||||
[pkgA, pkgB],
|
||||
notFoundPkgNames,
|
||||
);
|
||||
|
||||
expect(matchedPkgA.failedLoading).toBe(true);
|
||||
expect(matchedPkgB.failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should match failed packages even if version is wrong', () => {
|
||||
const [pkgA, pkgB] = mockPackagePair();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@123.456.789`;
|
||||
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
|
||||
[pkgA, pkgB],
|
||||
notFoundPackageList,
|
||||
);
|
||||
|
||||
expect(matchedPkgA.failedLoading).toBe(true);
|
||||
expect(matchedPkgB.failedLoading).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkNpmPackageStatus()', () => {
|
||||
test('should call axios.post', async () => {
|
||||
await communityPackageService.checkNpmPackageStatus(mockPackageName());
|
||||
|
||||
expect(axios.post).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not fail if request fails', async () => {
|
||||
mocked(axios.post).mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
const result = await communityPackageService.checkNpmPackageStatus(mockPackageName());
|
||||
|
||||
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
||||
});
|
||||
|
||||
test('should warn if package is banned', async () => {
|
||||
mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } });
|
||||
|
||||
const result = (await communityPackageService.checkNpmPackageStatus(
|
||||
mockPackageName(),
|
||||
)) as CommunityPackages.PackageStatusCheck;
|
||||
|
||||
expect(result.status).toBe('Banned');
|
||||
expect(result.reason).toBe('Not good');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPackageLoadedSuccessfully()', () => {
|
||||
test('should return true when failed package list does not exist', () => {
|
||||
config.set<string>('nodes.packagesMissing', undefined);
|
||||
|
||||
expect(communityPackageService.hasPackageLoaded('package')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true when package is not in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
|
||||
|
||||
expect(communityPackageService.hasPackageLoaded('packageC')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when package is in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
|
||||
|
||||
expect(communityPackageService.hasPackageLoaded('packageA')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePackageFromMissingList()', () => {
|
||||
test('should do nothing if key does not exist', () => {
|
||||
config.set<string>('nodes.packagesMissing', undefined);
|
||||
|
||||
communityPackageService.removePackageFromMissingList('packageA');
|
||||
|
||||
expect(config.get('nodes.packagesMissing')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should remove only correct package from list', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageC@0.2.0');
|
||||
|
||||
communityPackageService.removePackageFromMissingList('packageB');
|
||||
|
||||
expect(config.get('nodes.packagesMissing')).toBe('packageA@0.1.0 packageC@0.2.0');
|
||||
});
|
||||
|
||||
test('should not remove if package is not in the list', () => {
|
||||
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageB@0.2.0';
|
||||
config.set('nodes.packagesMissing', failedToLoadList);
|
||||
communityPackageService.removePackageFromMissingList('packageC');
|
||||
|
||||
expect(config.get('nodes.packagesMissing')).toBe(failedToLoadList);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user