n8n/packages/cli/test/integration/nodes.api.test.ts

251 lines
7.6 KiB
TypeScript

import path from 'path';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { Push } from '@/push';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import * as testDb from './shared/testDb';
import {
mockInstance,
setupTestServer,
mockPackage,
mockNode,
mockPackageName,
} from './shared/utils';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import type { SuperAgentTest } from 'supertest';
const communityPackageService = mockInstance(CommunityPackageService);
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
mockInstance(Push);
const testServer = setupTestServer({ endpointGroups: ['nodes'] });
const commonUpdatesProps = {
createdAt: new Date(),
updatedAt: new Date(),
installedVersion: COMMUNITY_PACKAGE_VERSION.CURRENT,
updateAvailable: COMMUNITY_PACKAGE_VERSION.UPDATED,
};
const parsedNpmPackageName = {
packageName: 'test',
rawString: 'test',
};
let authAgent: SuperAgentTest;
beforeAll(async () => {
const ownerShell = await testDb.createOwner();
authAgent = testServer.authAgentFor(ownerShell);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /nodes', () => {
test('should respond 200 if no nodes are installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([]);
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
expect(data).toHaveLength(0);
});
test('should return list of one installed package and node', async () => {
const pkg = mockPackage();
const node = mockNode(pkg.packageName);
pkg.installedNodes = [node];
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]);
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
});
test('should return list of multiple installed packages and nodes', async () => {
const pkgA = mockPackage();
const nodeA = mockNode(pkgA.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 {
body: { data },
} = await authAgent.get('/nodes').expect(200);
expect(data).toHaveLength(2);
const allNodes = data.reduce(
(acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes),
[],
);
expect(allNodes).toHaveLength(3);
});
test('should not check for updates if no packages installed', async () => {
await authAgent.get('/nodes');
expect(communityPackageService.executeNpmCommand).not.toHaveBeenCalled();
});
test('should check for updates if packages installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
await authAgent.get('/nodes').expect(200);
const args = ['npm outdated --json', { doNotHandleError: true }];
expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args);
});
test('should report package updates if available', async () => {
const pkg = mockPackage();
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.executeNpmCommand.mockImplementation(() => {
throw {
code: 1,
stdout: JSON.stringify({
[pkg.packageName]: {
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
location: path.join('node_modules', pkg.packageName),
},
}),
};
});
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
{
packageName: 'test',
installedNodes: [],
...commonUpdatesProps,
},
]);
const {
body: { data },
} = await authAgent.get('/nodes').expect(200);
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 () => {
await authAgent.post('/nodes').expect(400);
});
test('should reject if package is duplicate', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.isPackageInstalled.mockResolvedValue(true);
communityPackageService.hasPackageLoaded.mockReturnValue(true);
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const {
body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('already installed');
});
test('should allow installing packages that could not be loaded', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.hasPackageLoaded.mockReturnValue(false);
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage());
await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(200);
expect(communityPackageService.removePackageFromMissingList).toHaveBeenCalled();
});
test('should not install a banned package', async () => {
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const {
body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('banned');
});
});
describe('DELETE /nodes', () => {
test('should not delete if package name is empty', async () => {
await authAgent.delete('/nodes').expect(400);
});
test('should reject if package is not installed', async () => {
const {
body: { message },
} = await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(400);
expect(message).toContain('not installed');
});
test('should uninstall package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(200);
expect(mockLoadNodesAndCredentials.removeNpmModule).toHaveBeenCalledTimes(1);
});
});
describe('PATCH /nodes', () => {
test('should reject if package name is empty', async () => {
await authAgent.patch('/nodes').expect(400);
});
test('should reject if package is not installed', async () => {
const {
body: { message },
} = await authAgent.patch('/nodes').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('not installed');
});
test('should update a package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
await authAgent.patch('/nodes').send({ name: mockPackageName() });
expect(mockLoadNodesAndCredentials.updateNpmModule).toHaveBeenCalledTimes(1);
});
});