import path from 'path'; import type { SuperAgentTest } from 'supertest'; import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledNodes } from '@db/entities/InstalledNodes'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { Push } from '@/push'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { mockInstance } from '../shared/mocking'; import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; import { setupTestServer, mockPackage, mockNode, mockPackageName } from './shared/utils'; import { createOwner } from './shared/db/users'; const communityPackagesService = mockInstance(CommunityPackagesService, { hasMissingPackages: false, }); mockInstance(LoadNodesAndCredentials); mockInstance(Push); const testServer = setupTestServer({ endpointGroups: ['community-packages'] }); 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 createOwner(); authAgent = testServer.authAgentFor(ownerShell); }); beforeEach(() => { jest.resetAllMocks(); }); describe('GET /community-packages', () => { test('should respond 200 if no nodes are installed', async () => { communityPackagesService.getAllInstalledPackages.mockResolvedValue([]); const { body: { data }, } = await authAgent.get('/community-packages').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]; communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]); communityPackagesService.matchPackagesWithUpdates.mockReturnValue([pkg]); const { body: { data }, } = await authAgent.get('/community-packages').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); communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]); communityPackagesService.matchPackagesWithUpdates.mockReturnValue([ { ...commonUpdatesProps, packageName: pkgA.packageName, installedNodes: [nodeA], }, { ...commonUpdatesProps, packageName: pkgB.packageName, installedNodes: [nodeB, nodeC], }, ]); const { body: { data }, } = await authAgent.get('/community-packages').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('/community-packages'); expect(communityPackagesService.executeNpmCommand).not.toHaveBeenCalled(); }); test('should check for updates if packages installed', async () => { communityPackagesService.getAllInstalledPackages.mockResolvedValue([mockPackage()]); await authAgent.get('/community-packages').expect(200); const args = ['npm outdated --json', { doNotHandleError: true }]; expect(communityPackagesService.executeNpmCommand).toHaveBeenCalledWith(...args); }); test('should report package updates if available', async () => { const pkg = mockPackage(); communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]); communityPackagesService.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), }, }), }; }); communityPackagesService.matchPackagesWithUpdates.mockReturnValue([ { packageName: 'test', installedNodes: [], ...commonUpdatesProps, }, ]); const { body: { data }, } = await authAgent.get('/community-packages').expect(200); const [returnedPkg] = data; expect(returnedPkg.installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT); expect(returnedPkg.updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED); }); }); describe('POST /community-packages', () => { test('should reject if package name is missing', async () => { await authAgent.post('/community-packages').expect(400); }); test('should reject if package is duplicate', async () => { communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.isPackageInstalled.mockResolvedValue(true); communityPackagesService.hasPackageLoaded.mockReturnValue(true); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); const { body: { message }, } = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400); expect(message).toContain('already installed'); }); test('should allow installing packages that could not be loaded', async () => { communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.hasPackageLoaded.mockReturnValue(false); communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' }); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); communityPackagesService.installNpmModule.mockResolvedValue(mockPackage()); await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200); expect(communityPackagesService.removePackageFromMissingList).toHaveBeenCalled(); }); test('should not install a banned package', async () => { communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' }); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); const { body: { message }, } = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400); expect(message).toContain('banned'); }); }); describe('DELETE /community-packages', () => { test('should not delete if package name is empty', async () => { await authAgent.delete('/community-packages').expect(400); }); test('should reject if package is not installed', async () => { const { body: { message }, } = await authAgent .delete('/community-packages') .query({ name: mockPackageName() }) .expect(400); expect(message).toContain('not installed'); }); test('should uninstall package', async () => { communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200); expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1); }); }); describe('PATCH /community-packages', () => { test('should reject if package name is empty', async () => { await authAgent.patch('/community-packages').expect(400); }); test('should reject if package is not installed', async () => { const { body: { message }, } = await authAgent.patch('/community-packages').send({ name: mockPackageName() }).expect(400); expect(message).toContain('not installed'); }); test('should update a package', async () => { communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); await authAgent.patch('/community-packages').send({ name: mockPackageName() }); expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1); }); });