/********************************************************************** * Copyright (C) 2023-2024 Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ import type { SimpleContainerInfo } from '@podman-desktop/core-api'; import type Electron from 'electron'; import type { IpcMainEvent } from 'electron'; import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import type { ContainerProviderRegistry } from '/@/plugin/container-registry.js'; import type { ContributionManager } from '/@/plugin/contribution-manager.js'; import { DockerPluginAdapter } from './docker-plugin-adapter.js'; let dockerPluginAdapter: TestDockerPluginAdapter; vi.mock(import('electron'), () => { const mockIpcMain = { on: vi.fn().mockReturnThis(), }; return { ipcMain: mockIpcMain } as unknown as typeof Electron; }); const contributionManager = {} as unknown as ContributionManager; class TestDockerPluginAdapter extends DockerPluginAdapter { override async getVmServiceContainer(contributionId: string): Promise { return super.getVmServiceContainer(contributionId); } } const listSimpleContainersMock = vi.fn(); const execInContainerMock = vi.fn(); const containerProviderRegistry = { listSimpleContainers: listSimpleContainersMock, execInContainer: execInContainerMock, } as unknown as ContainerProviderRegistry; beforeAll(async () => { dockerPluginAdapter = new TestDockerPluginAdapter(contributionManager, containerProviderRegistry); }); beforeEach(() => { vi.clearAllMocks(); }); describe('getVmServiceContainer', async () => { test('Check getVmServiceContainer being found', async () => { const container1 = { Labels: {}, }; // another app const container2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': 'fooBar', }, }; const extensionName = 'myExtension'; // 2 plugins, only one is the VM service container const pluginContainer1 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, }, }; // this is the container being used as service container const pluginServiceContainer2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, }; listSimpleContainersMock.mockResolvedValueOnce([container1, container2, pluginContainer1, pluginServiceContainer2]); const result = await dockerPluginAdapter.getVmServiceContainer('myExtension'); expect(result).toBeDefined(); expect(result).toEqual(pluginServiceContainer2); }); test('Check getVmServiceContainer app not found', async () => { const container1 = { Labels: {}, }; // another app const container2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': 'fooBar', }, }; const extensionName = 'myExtension'; // 2 plugins, only one is the VM service container const pluginContainer1 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, }, }; // this is the container being used as service container const pluginServiceContainer2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, }; listSimpleContainersMock.mockResolvedValueOnce([container1, container2, pluginContainer1, pluginServiceContainer2]); await expect(dockerPluginAdapter.getVmServiceContainer('anotherApp')).rejects.toThrowError( `No container having the label 'io.podman_desktop.PodmanDesktop.extensionName'`, ); }); test('Check getVmServiceContainer vm service not found', async () => { const container1 = { Labels: {}, }; // another app const container2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': 'fooBar', }, }; const extensionName = 'myExtension'; // 2 plugins, only one is the VM service container const pluginContainer1 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, }, }; // this is the container being used as service container const pluginServiceContainer2 = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'false', }, }; listSimpleContainersMock.mockResolvedValueOnce([container1, container2, pluginContainer1, pluginServiceContainer2]); await expect(dockerPluginAdapter.getVmServiceContainer('myExtension')).rejects.toThrowError( `No container having the label 'io.podman_desktop.PodmanDesktop.vm-service' == 'true' found.`, ); }); }); describe('handle exec', async () => { test('handle exec inside the VM', async () => { const extensionName = 'myExtension'; // this is the container being used as service container const pluginServiceContainer = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, } as unknown as SimpleContainerInfo; const spyGetVmServiceContainer = vi.spyOn(dockerPluginAdapter, 'getVmServiceContainer'); spyGetVmServiceContainer.mockResolvedValueOnce(pluginServiceContainer); // mock execution execInContainerMock.mockImplementation( async ( _engineId: string, _id: string, _command: string[], onStdout: (data: Buffer) => void, onStderr: (data: Buffer) => void, ) => { // write hello world as stdout onStdout(Buffer.from('hello\n')); onStdout(Buffer.from('world\n')); // write warning as stderr onStderr(Buffer.from('warning: text1\n')); onStderr(Buffer.from('warning: text2\n')); }, ); const result = await dockerPluginAdapter.handleExec(extensionName, 'VM_SERVICE', 'echo', ['hello', 'world']); expect(result.code).toEqual(0); expect(result.stdout).toEqual('hello\nworld\n'); expect(result.stderr).toEqual('warning: text1\nwarning: text2\n'); }); test('handle exec with error inside the VM', async () => { const extensionName = 'myExtension'; // this is the container being used as service container const pluginServiceContainer = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, } as unknown as SimpleContainerInfo; const spyGetVmServiceContainer = vi.spyOn(dockerPluginAdapter, 'getVmServiceContainer'); spyGetVmServiceContainer.mockResolvedValueOnce(pluginServiceContainer); // mock execution execInContainerMock.mockImplementation( async ( _engineId: string, _id: string, _command: string[], _onStdout: (data: Buffer) => void, _onStderr: (data: Buffer) => void, ) => { throw new Error('This is a test error'); }, ); const result = await dockerPluginAdapter.handleExec(extensionName, 'VM_SERVICE', 'echo', ['hello', 'world']); expect(result.code).toEqual(1); expect(result.stdout).toEqual(''); expect(result.signal).toEqual('Error: This is a test error'); }); }); describe('handle execWithOptions', async () => { test('handle execWithOptions inside the VM', async () => { const extensionName = 'myExtension'; // this is the container being used as service container const pluginServiceContainer = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, } as unknown as SimpleContainerInfo; const spyGetVmServiceContainer = vi.spyOn(dockerPluginAdapter, 'getVmServiceContainer'); spyGetVmServiceContainer.mockResolvedValueOnce(pluginServiceContainer); // mock execution execInContainerMock.mockImplementation( async ( _engineId: string, _id: string, _command: string[], onStdout: (data: Buffer) => void, onStderr: (data: Buffer) => void, ) => { // write hello world as stdout onStdout(Buffer.from('hello')); onStdout(Buffer.from('world\n')); // write warning as stderr onStderr(Buffer.from('warning: text1\n')); onStderr(Buffer.from('warning: text2\n')); }, ); const replyMock = vi.fn(); const ipcMainEventMock = { reply: replyMock, } as unknown as IpcMainEvent; const callbackId = 123; await dockerPluginAdapter.handleExecWithOptions( ipcMainEventMock, extensionName, 'VM_SERVICE', 'echo', callbackId, {}, ['hello', 'world'], ); // check event reply expect(replyMock).toHaveBeenNthCalledWith( 1, 'docker-plugin-adapter:execWithOptions-callback-stdout', callbackId, Buffer.from('hello'), ); expect(replyMock).toHaveBeenNthCalledWith( 2, 'docker-plugin-adapter:execWithOptions-callback-stdout', callbackId, Buffer.from('world\n'), ); expect(replyMock).toHaveBeenNthCalledWith( 3, 'docker-plugin-adapter:execWithOptions-callback-stderr', callbackId, Buffer.from('warning: text1\n'), ); expect(replyMock).toHaveBeenNthCalledWith( 4, 'docker-plugin-adapter:execWithOptions-callback-stderr', callbackId, Buffer.from('warning: text2\n'), ); expect(replyMock).toHaveBeenNthCalledWith(5, 'docker-plugin-adapter:execWithOptions-callback-close', callbackId, 0); }); test('handle execWithOptions inside the VM with error', async () => { const error = new Error('custom error'); const extensionName = 'myExtension'; // this is the container being used as service container const pluginServiceContainer = { Labels: { 'io.podman_desktop.PodmanDesktop.extensionName': extensionName, 'io.podman_desktop.PodmanDesktop.vm-service': 'true', }, } as unknown as SimpleContainerInfo; const spyGetVmServiceContainer = vi.spyOn(dockerPluginAdapter, 'getVmServiceContainer'); spyGetVmServiceContainer.mockResolvedValueOnce(pluginServiceContainer); // mock execution execInContainerMock.mockImplementation(() => { throw error; }); const replyMock = vi.fn(); const ipcMainEventMock = { reply: replyMock, } as unknown as IpcMainEvent; const callbackId = 123; await dockerPluginAdapter.handleExecWithOptions( ipcMainEventMock, extensionName, 'VM_SERVICE', 'echo', callbackId, {}, ['hello', 'world'], ); // check event reply with error and close expect(replyMock).toHaveBeenNthCalledWith( 1, 'docker-plugin-adapter:execWithOptions-callback-error', callbackId, error, ); expect(replyMock).toHaveBeenNthCalledWith(2, 'docker-plugin-adapter:execWithOptions-callback-close', callbackId, 1); }); });