Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 0 additions & 93 deletions src/actions/containerDelete.test.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/actions/containerDelete.ts

This file was deleted.

128 changes: 128 additions & 0 deletions src/actions/containerLifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as vscode from 'vscode';
import { mock } from 'vitest-mock-extended';
import type { MockInstance } from 'vitest';
import { ProjectController } from '../controllers/projectController';
import { WrappedError } from '../errors/wrappedError';
import { ContainerCommands } from '../services/containerCommands';
import { ContainerItem } from '../util/types';
import { ContainerTreeItem } from '../views/treeItems/containerTreeItem';
import { ContainerLifecycle } from './containerLifecycle';

type LifecycleCase = {
operation: 'start' | 'stop' | 'delete';
command: 'startContainer' | 'stopContainer' | 'deleteContainer';
invoke: (
lifecycle: ContainerLifecycle,
treeItem: ContainerTreeItem,
) => Promise<void>;
};

const lifecycleCases = [
{
operation: 'start',
command: 'startContainer',
invoke: (lifecycle, treeItem) =>
lifecycle.startContainerCommandHandler(treeItem),
},
{
operation: 'stop',
command: 'stopContainer',
invoke: (lifecycle, treeItem) =>
lifecycle.stopContainerCommandHandler(treeItem),
},
{
operation: 'delete',
command: 'deleteContainer',
invoke: (lifecycle, treeItem) =>
lifecycle.deleteContainerCommandHandler(treeItem),
},
] satisfies LifecycleCase[];

describe('ContainerLifecycle', () => {
let showErrorMessageSpy: MockInstance;
const target = 'user@topo.local';
const container: ContainerItem = {
id: 'abc123',
names: 'my-container',
image: 'nginx',
state: 'running',
status: 'Up',
processingDomain: 'CoolProcessingDomain',
address: '1.2.3.4:5678',
target,
};
const treeItem = new ContainerTreeItem(container);

beforeEach(() => {
showErrorMessageSpy = vi
.spyOn(vscode.window, 'showErrorMessage')
.mockImplementation(vi.fn());
});

afterEach(() => {
vi.clearAllMocks();
});

it.each(lifecycleCases)(
'$operation invokes the matching command and refreshes containers',
async ({ command, invoke }) => {
const containerCommands = mock<ContainerCommands>();
const projectController = mock<ProjectController>();
const lifecycle = new ContainerLifecycle(
containerCommands,
projectController,
);

await invoke(lifecycle, treeItem);

expect(containerCommands[command]).toHaveBeenCalledWith(
container.id,
target,
);
expect(
projectController.refreshProjectContainersCommandHandler,
).toHaveBeenCalledOnce();
},
);

it.each(lifecycleCases)(
'$operation reports Docker errors without refreshing containers',
async ({ operation, command, invoke }) => {
const containerCommands = mock<ContainerCommands>();
const projectController = mock<ProjectController>();
containerCommands[command].mockRejectedValue(
new WrappedError('DOCKER', 'fail'),
);
const lifecycle = new ContainerLifecycle(
containerCommands,
projectController,
);

await invoke(lifecycle, treeItem);

expect(showErrorMessageSpy).toHaveBeenCalledWith(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(showErrorMessageSpy).toHaveBeenCalledWith(
expect(vi.mocked(vscode.window.showErrorMessage)).toHaveBeenCalledWith(

Allows for deletion of the manual spy and beforeEach/afterEach blocks

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect.stringContaining(
`Failed to ${operation} the container ${container.id}. fail`,
),
);
expect(
projectController.refreshProjectContainersCommandHandler,
).not.toHaveBeenCalled();
},
);

it('rethrows unexpected command errors', async () => {
const containerCommands = mock<ContainerCommands>();
containerCommands.startContainer.mockRejectedValue(
new Error('generic error'),
);
const lifecycle = new ContainerLifecycle(
containerCommands,
mock<ProjectController>(),
);

await expect(
lifecycle.startContainerCommandHandler(treeItem),
).rejects.toThrow('generic error');
});
});
63 changes: 63 additions & 0 deletions src/actions/containerLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ProjectController } from '../controllers/projectController';
import { isWrappedError } from '../errors/wrappedError';
import { ContainerCommands } from '../services/containerCommands';
import { showAndLogError } from '../util/showAndLog';
import { assertContainerTreeItem } from '../views/treeItems/assertContainerTreeItem';

const containerOperationMethods = {
start: 'startContainer',
stop: 'stopContainer',
delete: 'deleteContainer',
} as const;

type ContainerOperation = keyof typeof containerOperationMethods;

export class ContainerLifecycle {
constructor(
private readonly containerCommands: ContainerCommands,
private readonly projectController: ProjectController,
) {}

public async startContainerCommandHandler(
treeNode: unknown,
): Promise<void> {
await this.runContainerCommand('start', treeNode);
}

public async stopContainerCommandHandler(treeNode: unknown): Promise<void> {
await this.runContainerCommand('stop', treeNode);
}

public async deleteContainerCommandHandler(
treeNode: unknown,
): Promise<void> {
await this.runContainerCommand('delete', treeNode);
}

private async runContainerCommand(
operation: ContainerOperation,
treeNode: unknown,
): Promise<void> {
assertContainerTreeItem(treeNode);
const containerId = treeNode.containerItem.id;
const commandMethod = containerOperationMethods[operation];

try {
await this.containerCommands[commandMethod](
containerId,
treeNode.containerItem.target,
);
} catch (error: unknown) {
if (isWrappedError(error, ['DOCKER'])) {
showAndLogError(
`Failed to ${operation} the container ${containerId}`,
error,
);
return;
}
throw error;
}

await this.projectController.refreshProjectContainersCommandHandler();
}
}
Loading