Skip to content

Commit 9936480

Browse files
authored
refactor: provider-registry:create(Container|Kubernetes)ProviderConnection (podman-desktop#11809)
* refactor: create*ProviderConnection Signed-off-by: Philippe Martin <phmartin@redhat.com> * fix: review Signed-off-by: Philippe Martin <phmartin@redhat.com> * refactor: create TaskConnectionUtils Signed-off-by: Philippe Martin <phmartin@redhat.com> * fix: review Signed-off-by: Philippe Martin <phmartin@redhat.com>
1 parent e70ed8e commit 9936480

File tree

4 files changed

+335
-63
lines changed

4 files changed

+335
-63
lines changed

packages/main/src/plugin/index.spec.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import { tmpdir } from 'node:os';
2323
import type { PullEvent } from '@podman-desktop/api';
2424
import type { WebContents } from 'electron';
2525
import { app, BrowserWindow, clipboard, ipcMain, shell } from 'electron';
26-
import { beforeAll, beforeEach, expect, test, vi } from 'vitest';
26+
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2727

2828
import { ExtensionLoader } from '/@/plugin/extension/extension-loader.js';
2929
import { Updater } from '/@/plugin/updater.js';
3030
import type { NotificationCardOptions } from '/@api/notification.js';
31+
import type { ProviderInfo } from '/@api/provider-info.js';
3132

3233
import { securityRestrictionCurrentHandler } from '../security-restrictions-handler.js';
3334
import type { TrayMenu } from '../tray-menu.js';
@@ -37,9 +38,14 @@ import type { ConfigurationRegistry } from './configuration-registry.js';
3738
import { ContainerProviderRegistry } from './container-registry.js';
3839
import type { Directories } from './directories.js';
3940
import { Emitter } from './events/emitter.js';
41+
import type { LoggerWithEnd } from './index.js';
4042
import { PluginSystem } from './index.js';
4143
import type { MessageBox } from './message-box.js';
44+
import { NavigationManager } from './navigation/navigation-manager.js';
45+
import { ProviderRegistry } from './provider-registry.js';
46+
import { TaskImpl } from './tasks/task-impl.js';
4247
import { TaskManager } from './tasks/task-manager.js';
48+
import type { Task } from './tasks/tasks.js';
4349
import { Disposable } from './types/disposable.js';
4450
import { Deferred } from './util/deferred.js';
4551
import { HttpServer } from './webview/webview-registry.js';
@@ -413,3 +419,97 @@ test('ipcMain.handle returns caught error as objects message property if it is n
413419
const handleReturn = await handle(undefined, '1');
414420
expect(handleReturn.error).toEqual({ message: nonErrorInstance });
415421
});
422+
423+
describe.each<{
424+
handler: string;
425+
methodName: 'createContainerProviderConnection' | 'createKubernetesProviderConnection';
426+
}>([
427+
{
428+
handler: 'provider-registry:createContainerProviderConnection',
429+
methodName: 'createContainerProviderConnection',
430+
},
431+
{
432+
handler: 'provider-registry:createKubernetesProviderConnection',
433+
methodName: 'createKubernetesProviderConnection',
434+
},
435+
])('$handler', async ({ handler, methodName }) => {
436+
let originalTask: Task;
437+
438+
beforeEach(() => {
439+
originalTask = {
440+
status: 'in-progress',
441+
error: '',
442+
} as unknown as Task;
443+
vi.spyOn(TaskManager.prototype, 'createTask').mockReturnValue(originalTask);
444+
vi.spyOn(NavigationManager.prototype, 'navigateToProviderTask');
445+
vi.spyOn(ProviderRegistry.prototype, 'getProviderInfo').mockReturnValue({
446+
name: 'provider1',
447+
} as ProviderInfo);
448+
});
449+
450+
test('createTask is called', async () => {
451+
await pluginSystem.initExtensions(new Emitter<ConfigurationRegistry>());
452+
const handle = handlers.get(handler);
453+
expect(handle).not.equal(undefined);
454+
await handle(undefined, 'internal1', { key1: 'value1', key2: 42 }, 'logger1', 'token1', 'task1');
455+
expect(TaskManager.prototype.createTask).toHaveBeenCalledOnce();
456+
const params = vi.mocked(TaskManager.prototype.createTask).mock.calls[0]?.[0];
457+
if (!params) {
458+
// this is already expected
459+
throw new Error('param should be defined');
460+
}
461+
expect(params.title).toEqual(`Creating provider1 provider`);
462+
expect(params.action?.name).toEqual(`Open task`);
463+
464+
// check that action.execute passed to createTask is calling navigateToProviderTask
465+
const execute = params.action?.execute;
466+
expect(execute).toBeDefined();
467+
if (!execute) {
468+
throw new Error('execute should be defined');
469+
}
470+
execute(new TaskImpl('task1id', 'task1name'));
471+
expect(NavigationManager.prototype.navigateToProviderTask).toHaveBeenCalledOnce();
472+
expect(NavigationManager.prototype.navigateToProviderTask).toHaveBeenCalledWith('internal1', 'task1');
473+
});
474+
475+
test(`${methodName} is called and is resolved`, async () => {
476+
vi.spyOn(ProviderRegistry.prototype, methodName).mockResolvedValue();
477+
const onEndMock = vi.fn();
478+
const errorMock = vi.fn();
479+
vi.spyOn(pluginSystem, 'getLogHandler').mockReturnValue({
480+
onEnd: onEndMock,
481+
error: errorMock,
482+
} as unknown as LoggerWithEnd);
483+
await pluginSystem.initExtensions(new Emitter<ConfigurationRegistry>());
484+
const handle = handlers.get(handler);
485+
expect(handle).not.equal(undefined);
486+
const result = await handle(undefined, 'internal1', { key1: 'value1', key2: 42 }, 'logger1', 'token1', 'task1');
487+
expect(result).toEqual({ result: undefined });
488+
expect(onEndMock).toHaveBeenCalled();
489+
expect(errorMock).not.toHaveBeenCalled();
490+
expect(originalTask.status).toEqual('success');
491+
expect(originalTask.error).toEqual('');
492+
});
493+
494+
test(`${methodName} is called and is rejected`, async () => {
495+
const rejectError = new Error('an error');
496+
vi.spyOn(ProviderRegistry.prototype, methodName).mockRejectedValue(rejectError);
497+
const onEndMock = vi.fn();
498+
const errorMock = vi.fn();
499+
vi.spyOn(pluginSystem, 'getLogHandler').mockReturnValue({
500+
onEnd: onEndMock,
501+
error: errorMock,
502+
} as unknown as LoggerWithEnd);
503+
await pluginSystem.initExtensions(new Emitter<ConfigurationRegistry>());
504+
const handle = handlers.get(handler);
505+
expect(handle).not.equal(undefined);
506+
const result = await handle(undefined, 'internal1', { key1: 'value1', key2: 42 }, 'logger1', 'token1', 'task1');
507+
expect(result).toEqual({
508+
error: rejectError,
509+
});
510+
expect(onEndMock).toHaveBeenCalled();
511+
expect(errorMock).toHaveBeenCalledWith(rejectError);
512+
expect(originalTask.status).toEqual('in-progress');
513+
expect(originalTask.error).toEqual('Something went wrong while trying to create provider: Error: an error');
514+
});
515+
});

packages/main/src/plugin/index.ts

Lines changed: 21 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ import type { IDisposable } from './types/disposable.js';
198198
import type { Deferred } from './util/deferred.js';
199199
import { Exec } from './util/exec.js';
200200
import { getFreePort, getFreePortRange, isFreePort } from './util/port.js';
201+
import { TaskConnectionUtils } from './util/task-connection-utils.js';
201202
import { ViewRegistry } from './view-registry.js';
202203
import { WebviewRegistry } from './webview/webview-registry.js';
203204
import { WelcomeInit } from './welcome/welcome-init.js';
@@ -2320,6 +2321,12 @@ export class PluginSystem {
23202321
},
23212322
);
23222323

2324+
const taskConnectionUtils = new TaskConnectionUtils({
2325+
getLogHandler: this.getLogHandler.bind(this),
2326+
cancellationTokenRegistry,
2327+
taskManager,
2328+
});
2329+
23232330
this.ipcHandle(
23242331
'provider-registry:createContainerProviderConnection',
23252332
async (
@@ -2330,40 +2337,16 @@ export class PluginSystem {
23302337
tokenId: number | undefined,
23312338
taskId: number | undefined,
23322339
): Promise<void> => {
2333-
const logger = this.getLogHandler('provider-registry:taskConnection-onData', loggerId);
2334-
let token;
2335-
if (tokenId) {
2336-
const tokenSource = cancellationTokenRegistry.getCancellationTokenSource(tokenId);
2337-
token = tokenSource?.token;
2338-
}
2339-
23402340
const providerName = providerRegistry.getProviderInfo(internalProviderId)?.name;
2341-
const task = taskManager.createTask({
2341+
return taskConnectionUtils.withTask({
2342+
loggerId,
2343+
tokenId,
23422344
title: `Creating ${providerName ?? 'Container'} provider`,
2343-
action: {
2344-
name: 'Open task',
2345-
execute: () => {
2346-
navigationManager
2347-
.navigateToProviderTask(internalProviderId, taskId)
2348-
.catch((err: unknown) => console.error(err));
2349-
},
2350-
},
2345+
navigateToTask: () => navigationManager.navigateToProviderTask(internalProviderId, taskId),
2346+
execute: (logger: LoggerWithEnd, token?: containerDesktopAPI.CancellationToken) =>
2347+
providerRegistry.createContainerProviderConnection(internalProviderId, params, logger, token),
2348+
executeErrorMsg: (err: unknown) => `Something went wrong while trying to create provider: ${err}`,
23512349
});
2352-
2353-
return providerRegistry
2354-
.createContainerProviderConnection(internalProviderId, params, logger, token)
2355-
.then(result => {
2356-
task.status = 'success';
2357-
return result;
2358-
})
2359-
.catch((err: unknown) => {
2360-
task.error = `Something went wrong while trying to create provider: ${err}`;
2361-
logger.error(err);
2362-
throw err;
2363-
})
2364-
.finally(() => {
2365-
logger.onEnd();
2366-
});
23672350
},
23682351
);
23692352

@@ -2388,40 +2371,16 @@ export class PluginSystem {
23882371
tokenId: number | undefined,
23892372
taskId: number | undefined,
23902373
): Promise<void> => {
2391-
const logger = this.getLogHandler('provider-registry:taskConnection-onData', loggerId);
2392-
let token;
2393-
if (tokenId) {
2394-
const tokenSource = cancellationTokenRegistry.getCancellationTokenSource(tokenId);
2395-
token = tokenSource?.token;
2396-
}
2397-
23982374
const providerName = providerRegistry.getProviderInfo(internalProviderId)?.name;
2399-
const task = taskManager.createTask({
2375+
return taskConnectionUtils.withTask({
2376+
loggerId,
2377+
tokenId,
24002378
title: `Creating ${providerName ?? 'Kubernetes'} provider`,
2401-
action: {
2402-
name: 'Open task',
2403-
execute: () => {
2404-
navigationManager
2405-
.navigateToProviderTask(internalProviderId, taskId)
2406-
.catch((err: unknown) => console.error(err));
2407-
},
2408-
},
2379+
navigateToTask: () => navigationManager.navigateToProviderTask(internalProviderId, taskId),
2380+
execute: (logger: LoggerWithEnd, token?: containerDesktopAPI.CancellationToken) =>
2381+
providerRegistry.createKubernetesProviderConnection(internalProviderId, params, logger, token),
2382+
executeErrorMsg: (err: unknown) => `Something went wrong while trying to create provider: ${err}`,
24092383
});
2410-
2411-
return providerRegistry
2412-
.createKubernetesProviderConnection(internalProviderId, params, logger, token)
2413-
.then(result => {
2414-
task.status = 'success';
2415-
return result;
2416-
})
2417-
.catch((err: unknown) => {
2418-
task.error = `Something went wrong while trying to create provider: ${err}`;
2419-
logger.error(err);
2420-
throw err;
2421-
})
2422-
.finally(() => {
2423-
logger.onEnd();
2424-
});
24252384
},
24262385
);
24272386

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { beforeEach, expect, test, vi } from 'vitest';
20+
21+
import { CancellationTokenSource } from '../cancellation-token.js';
22+
import type { CancellationTokenRegistry } from '../cancellation-token-registry.js';
23+
import type { LoggerWithEnd } from '../index.js';
24+
import { TaskImpl } from '../tasks/task-impl.js';
25+
import type { TaskManager } from '../tasks/task-manager.js';
26+
import type { Task } from '../tasks/tasks.js';
27+
import type { TaskOptions } from './task-connection-utils.js';
28+
import { TaskConnectionUtils } from './task-connection-utils.js';
29+
30+
let originalTask: Task;
31+
let taskConnectionUtils: TaskConnectionUtils;
32+
33+
const getLogHandlerResult = {
34+
onEnd: vi.fn(),
35+
error: vi.fn(),
36+
} as unknown as LoggerWithEnd;
37+
38+
const cancellationTokenRegistry = {
39+
getCancellationTokenSource: vi.fn(),
40+
} as unknown as CancellationTokenRegistry;
41+
42+
const taskManager = {
43+
createTask: vi.fn(),
44+
} as unknown as TaskManager;
45+
46+
beforeEach(() => {
47+
vi.resetAllMocks();
48+
vi.restoreAllMocks();
49+
50+
originalTask = {
51+
status: 'in-progress',
52+
error: '',
53+
} as unknown as Task;
54+
55+
const options = {
56+
getLogHandler: vi.fn(),
57+
cancellationTokenRegistry,
58+
taskManager,
59+
};
60+
61+
vi.mocked(options.getLogHandler).mockReturnValue(getLogHandlerResult);
62+
vi.mocked(taskManager.createTask).mockReturnValue(originalTask);
63+
vi.mocked(cancellationTokenRegistry.getCancellationTokenSource).mockReturnValue(new CancellationTokenSource());
64+
65+
taskConnectionUtils = new TaskConnectionUtils(options);
66+
});
67+
68+
test('createTask is called', async () => {
69+
const options: TaskOptions = {
70+
loggerId: 'logger1',
71+
tokenId: 42,
72+
title: 'a title',
73+
navigateToTask: vi.fn(),
74+
execute: vi.fn(),
75+
executeErrorMsg: vi.fn(),
76+
};
77+
vi.mocked(options.execute).mockResolvedValue();
78+
vi.mocked(options.navigateToTask).mockResolvedValue();
79+
80+
await taskConnectionUtils.withTask(options);
81+
82+
expect(taskManager.createTask).toHaveBeenCalledOnce();
83+
const createTaskParams = vi.mocked(taskManager.createTask).mock.calls[0]?.[0];
84+
if (!createTaskParams) {
85+
throw new Error('param should be defined');
86+
}
87+
expect(createTaskParams.title).toEqual('a title');
88+
expect(createTaskParams.action?.name).toEqual(`Open task`);
89+
90+
// check that action.execute passed to createTask is calling options.navigateToTask
91+
const execute = createTaskParams.action?.execute;
92+
expect(execute).toBeDefined();
93+
if (!execute) {
94+
throw new Error('execute should be defined');
95+
}
96+
execute(new TaskImpl('id', 'name'));
97+
expect(options.navigateToTask).toHaveBeenCalled();
98+
});
99+
100+
test(`options.execute is called and is resolved`, async () => {
101+
const options: TaskOptions = {
102+
loggerId: 'logger1',
103+
tokenId: 42,
104+
title: 'a title',
105+
navigateToTask: vi.fn(),
106+
execute: vi.fn(),
107+
executeErrorMsg: vi.fn(),
108+
};
109+
vi.mocked(options.execute).mockResolvedValue();
110+
vi.mocked(options.navigateToTask).mockResolvedValue();
111+
112+
const result = await taskConnectionUtils.withTask(options);
113+
114+
expect(result).toBeUndefined();
115+
expect(getLogHandlerResult.onEnd).toHaveBeenCalled();
116+
expect(getLogHandlerResult.error).not.toHaveBeenCalled();
117+
expect(originalTask.status).toEqual('success');
118+
expect(originalTask.error).toEqual('');
119+
});
120+
121+
test(`options.execute is called and is rejected`, async () => {
122+
const rejectError = new Error('an error');
123+
124+
const options: TaskOptions = {
125+
loggerId: 'logger1',
126+
tokenId: 42,
127+
title: 'a title',
128+
navigateToTask: vi.fn(),
129+
execute: vi.fn(),
130+
executeErrorMsg: vi.fn(),
131+
};
132+
vi.mocked(options.execute).mockRejectedValue(rejectError);
133+
vi.mocked(options.navigateToTask).mockResolvedValue();
134+
vi.mocked(options.executeErrorMsg).mockReturnValue('an error msg');
135+
await expect(() => taskConnectionUtils.withTask(options)).rejects.toThrow();
136+
137+
expect(getLogHandlerResult.onEnd).toHaveBeenCalled();
138+
expect(getLogHandlerResult.error).toHaveBeenCalledWith(rejectError);
139+
expect(originalTask.status).toEqual('in-progress');
140+
expect(originalTask.error).toEqual('an error msg');
141+
});

0 commit comments

Comments
 (0)