Skip to content

Commit 7b2f369

Browse files
committed
Based on the git diff, here's a concise and descriptive commit message:
``` fix: enhance plugin management with interactive removal prompts - Add RemovePluginQuestionSet for interactive plugin removal - Update plugin commands to use PluginManagementService - Improve plugin installation error handling and warnings - Clean up test fixtures and update plugin command tests - Reset dev config to clean state (v4.11.0, no plugins)
1 parent 782d5eb commit 7b2f369

File tree

11 files changed

+197
-283
lines changed

11 files changed

+197
-283
lines changed

api/dev/configs/api.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
{
2-
"version": "4.10.0",
3-
"extraOrigins": [
4-
"https://google.com",
5-
"https://test.com"
6-
],
7-
"sandbox": true,
2+
"version": "4.11.0",
3+
"extraOrigins": [],
4+
"sandbox": false,
85
"ssoSubIds": [],
9-
"plugins": [
10-
"unraid-api-plugin-connect"
11-
]
6+
"plugins": []
127
}
Lines changed: 81 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Test } from '@nestjs/testing';
22

3+
import { InquirerService } from 'nest-commander';
34
import { beforeEach, describe, expect, it, vi } from 'vitest';
45

56
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
@@ -10,6 +11,8 @@ import {
1011
RemovePluginCommand,
1112
} from '@app/unraid-api/cli/plugins/plugin.command.js';
1213
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
14+
import { PluginManagementService } from '@app/unraid-api/plugin/plugin-management.service.js';
15+
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
1316

1417
// Mock services
1518
const mockInternalClient = {
@@ -20,12 +23,32 @@ const mockLogger = {
2023
log: vi.fn(),
2124
info: vi.fn(),
2225
error: vi.fn(),
26+
warn: vi.fn(),
27+
table: vi.fn(),
2328
};
2429

2530
const mockRestartCommand = {
2631
run: vi.fn(),
2732
};
2833

34+
const mockPluginManagementService = {
35+
addPlugin: vi.fn(),
36+
addBundledPlugin: vi.fn(),
37+
removePlugin: vi.fn(),
38+
removeBundledPlugin: vi.fn(),
39+
plugins: [] as string[],
40+
};
41+
42+
const mockInquirerService = {
43+
prompt: vi.fn(),
44+
};
45+
46+
vi.mock('@app/unraid-api/plugin/plugin.service.js', () => ({
47+
PluginService: {
48+
listPlugins: vi.fn(),
49+
},
50+
}));
51+
2952
describe('Plugin Commands', () => {
3053
beforeEach(() => {
3154
// Clear mocks before each test
@@ -39,94 +62,40 @@ describe('Plugin Commands', () => {
3962
const module = await Test.createTestingModule({
4063
providers: [
4164
InstallPluginCommand,
42-
{ provide: CliInternalClientService, useValue: mockInternalClient },
4365
{ provide: LogService, useValue: mockLogger },
4466
{ provide: RestartCommand, useValue: mockRestartCommand },
67+
{ provide: PluginManagementService, useValue: mockPluginManagementService },
4568
],
4669
}).compile();
4770

4871
command = module.get<InstallPluginCommand>(InstallPluginCommand);
4972
});
5073

5174
it('should install a plugin successfully', async () => {
52-
const mockClient = {
53-
mutate: vi.fn().mockResolvedValue({
54-
data: {
55-
addPlugin: false, // No manual restart required
56-
},
57-
}),
58-
};
59-
60-
mockInternalClient.getClient.mockResolvedValue(mockClient);
61-
6275
await command.run(['@unraid/plugin-example'], { bundled: false, restart: true });
6376

64-
expect(mockClient.mutate).toHaveBeenCalledWith({
65-
mutation: expect.anything(),
66-
variables: {
67-
input: {
68-
names: ['@unraid/plugin-example'],
69-
bundled: false,
70-
restart: true,
71-
},
72-
},
73-
});
77+
expect(mockPluginManagementService.addPlugin).toHaveBeenCalledWith('@unraid/plugin-example');
7478
expect(mockLogger.log).toHaveBeenCalledWith('Added plugin @unraid/plugin-example');
75-
expect(mockRestartCommand.run).not.toHaveBeenCalled(); // Because addPlugin returned false
79+
expect(mockRestartCommand.run).toHaveBeenCalled();
7680
});
7781

7882
it('should handle bundled plugin installation', async () => {
79-
const mockClient = {
80-
mutate: vi.fn().mockResolvedValue({
81-
data: {
82-
addPlugin: true, // Manual restart required
83-
},
84-
}),
85-
};
86-
87-
mockInternalClient.getClient.mockResolvedValue(mockClient);
88-
8983
await command.run(['@unraid/bundled-plugin'], { bundled: true, restart: true });
9084

91-
expect(mockClient.mutate).toHaveBeenCalledWith({
92-
mutation: expect.anything(),
93-
variables: {
94-
input: {
95-
names: ['@unraid/bundled-plugin'],
96-
bundled: true,
97-
restart: true,
98-
},
99-
},
100-
});
85+
expect(mockPluginManagementService.addBundledPlugin).toHaveBeenCalledWith(
86+
'@unraid/bundled-plugin'
87+
);
10188
expect(mockLogger.log).toHaveBeenCalledWith('Added bundled plugin @unraid/bundled-plugin');
102-
expect(mockRestartCommand.run).toHaveBeenCalled(); // Because addPlugin returned true
89+
expect(mockRestartCommand.run).toHaveBeenCalled();
10390
});
10491

10592
it('should not restart when restart option is false', async () => {
106-
const mockClient = {
107-
mutate: vi.fn().mockResolvedValue({
108-
data: {
109-
addPlugin: true,
110-
},
111-
}),
112-
};
113-
114-
mockInternalClient.getClient.mockResolvedValue(mockClient);
115-
11693
await command.run(['@unraid/plugin'], { bundled: false, restart: false });
11794

95+
expect(mockPluginManagementService.addPlugin).toHaveBeenCalledWith('@unraid/plugin');
11896
expect(mockRestartCommand.run).not.toHaveBeenCalled();
11997
});
12098

121-
it('should handle errors', async () => {
122-
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
123-
124-
await command.run(['@unraid/plugin'], { bundled: false, restart: true });
125-
126-
expect(mockLogger.error).toHaveBeenCalledWith('Failed to add plugin:', expect.any(Error));
127-
expect(process.exitCode).toBe(1);
128-
});
129-
13099
it('should error when no package name provided', async () => {
131100
await command.run([], { bundled: false, restart: true });
132101

@@ -142,57 +111,58 @@ describe('Plugin Commands', () => {
142111
const module = await Test.createTestingModule({
143112
providers: [
144113
RemovePluginCommand,
145-
{ provide: CliInternalClientService, useValue: mockInternalClient },
146114
{ provide: LogService, useValue: mockLogger },
115+
{ provide: PluginManagementService, useValue: mockPluginManagementService },
147116
{ provide: RestartCommand, useValue: mockRestartCommand },
117+
{ provide: InquirerService, useValue: mockInquirerService },
148118
],
149119
}).compile();
150120

151121
command = module.get<RemovePluginCommand>(RemovePluginCommand);
152122
});
153123

154-
it('should remove a plugin successfully', async () => {
155-
const mockClient = {
156-
mutate: vi.fn().mockResolvedValue({
157-
data: {
158-
removePlugin: false, // No manual restart required
159-
},
160-
}),
161-
};
124+
it('should remove plugins successfully', async () => {
125+
mockInquirerService.prompt.mockResolvedValue({
126+
plugins: ['@unraid/plugin-example', '@unraid/plugin-test'],
127+
restart: true,
128+
});
162129

163-
mockInternalClient.getClient.mockResolvedValue(mockClient);
130+
await command.run([], { restart: true });
164131

165-
await command.run(['@unraid/plugin-example'], { bundled: false, restart: true });
132+
expect(mockPluginManagementService.removePlugin).toHaveBeenCalledWith(
133+
'@unraid/plugin-example',
134+
'@unraid/plugin-test'
135+
);
136+
expect(mockLogger.log).toHaveBeenCalledWith('Removed plugin @unraid/plugin-example');
137+
expect(mockLogger.log).toHaveBeenCalledWith('Removed plugin @unraid/plugin-test');
138+
expect(mockRestartCommand.run).toHaveBeenCalled();
139+
});
166140

167-
expect(mockClient.mutate).toHaveBeenCalledWith({
168-
mutation: expect.anything(),
169-
variables: {
170-
input: {
171-
names: ['@unraid/plugin-example'],
172-
bundled: false,
173-
restart: true,
174-
},
175-
},
141+
it('should handle when no plugins are selected', async () => {
142+
mockInquirerService.prompt.mockResolvedValue({
143+
plugins: [],
144+
restart: true,
176145
});
177-
expect(mockLogger.log).toHaveBeenCalledWith('Removed plugin @unraid/plugin-example');
146+
147+
await command.run([], { restart: true });
148+
149+
expect(mockLogger.warn).toHaveBeenCalledWith('No plugins selected for removal.');
150+
expect(mockPluginManagementService.removePlugin).not.toHaveBeenCalled();
178151
expect(mockRestartCommand.run).not.toHaveBeenCalled();
179152
});
180153

181-
it('should handle removing bundled plugins', async () => {
182-
const mockClient = {
183-
mutate: vi.fn().mockResolvedValue({
184-
data: {
185-
removePlugin: true, // Manual restart required
186-
},
187-
}),
188-
};
154+
it('should skip restart when --no-restart is specified', async () => {
155+
mockInquirerService.prompt.mockResolvedValue({
156+
plugins: ['@unraid/plugin-example'],
157+
restart: false,
158+
});
189159

190-
mockInternalClient.getClient.mockResolvedValue(mockClient);
160+
await command.run([], { restart: false });
191161

192-
await command.run(['@unraid/bundled-plugin'], { bundled: true, restart: true });
193-
194-
expect(mockLogger.log).toHaveBeenCalledWith('Removed bundled plugin @unraid/bundled-plugin');
195-
expect(mockRestartCommand.run).toHaveBeenCalled();
162+
expect(mockPluginManagementService.removePlugin).toHaveBeenCalledWith(
163+
'@unraid/plugin-example'
164+
);
165+
expect(mockRestartCommand.run).not.toHaveBeenCalled();
196166
});
197167
});
198168

@@ -203,72 +173,46 @@ describe('Plugin Commands', () => {
203173
const module = await Test.createTestingModule({
204174
providers: [
205175
ListPluginCommand,
206-
{ provide: CliInternalClientService, useValue: mockInternalClient },
207176
{ provide: LogService, useValue: mockLogger },
177+
{ provide: PluginManagementService, useValue: mockPluginManagementService },
208178
],
209179
}).compile();
210180

211181
command = module.get<ListPluginCommand>(ListPluginCommand);
212182
});
213183

214184
it('should list installed plugins', async () => {
215-
const mockClient = {
216-
query: vi.fn().mockResolvedValue({
217-
data: {
218-
plugins: [
219-
{
220-
name: '@unraid/plugin-1',
221-
version: '1.0.0',
222-
hasApiModule: true,
223-
hasCliModule: false,
224-
},
225-
{
226-
name: '@unraid/plugin-2',
227-
version: '2.0.0',
228-
hasApiModule: true,
229-
hasCliModule: true,
230-
},
231-
],
232-
},
233-
}),
234-
};
235-
236-
mockInternalClient.getClient.mockResolvedValue(mockClient);
185+
vi.mocked(PluginService.listPlugins).mockResolvedValue([
186+
['@unraid/plugin-1', '1.0.0'],
187+
['@unraid/plugin-2', '2.0.0'],
188+
]);
189+
mockPluginManagementService.plugins = ['@unraid/plugin-1', '@unraid/plugin-2'];
237190

238191
await command.run();
239192

240-
expect(mockClient.query).toHaveBeenCalledWith({
241-
query: expect.anything(),
242-
});
243193
expect(mockLogger.log).toHaveBeenCalledWith('Installed plugins:\n');
244-
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/[email protected] [API]');
245-
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/[email protected] [API, CLI]');
194+
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/[email protected]');
195+
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/[email protected]');
246196
expect(mockLogger.log).toHaveBeenCalledWith();
247197
});
248198

249199
it('should handle no plugins installed', async () => {
250-
const mockClient = {
251-
query: vi.fn().mockResolvedValue({
252-
data: {
253-
plugins: [],
254-
},
255-
}),
256-
};
257-
258-
mockInternalClient.getClient.mockResolvedValue(mockClient);
200+
vi.mocked(PluginService.listPlugins).mockResolvedValue([]);
201+
mockPluginManagementService.plugins = [];
259202

260203
await command.run();
261204

262205
expect(mockLogger.log).toHaveBeenCalledWith('No plugins installed.');
263206
});
264207

265-
it('should handle errors', async () => {
266-
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
208+
it('should warn about plugins not installed', async () => {
209+
vi.mocked(PluginService.listPlugins).mockResolvedValue([['@unraid/plugin-1', '1.0.0']]);
210+
mockPluginManagementService.plugins = ['@unraid/plugin-1', '@unraid/plugin-2'];
267211

268212
await command.run();
269213

270-
expect(mockLogger.error).toHaveBeenCalledWith('Failed to list plugins:', expect.any(Error));
271-
expect(process.exitCode).toBe(1);
214+
expect(mockLogger.warn).toHaveBeenCalledWith('1 plugins are not installed:');
215+
expect(mockLogger.table).toHaveBeenCalledWith('warn', ['@unraid/plugin-2']);
272216
});
273217
});
274218
});

api/src/unraid-api/cli/cli.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
PluginCommand,
2222
RemovePluginCommand,
2323
} from '@app/unraid-api/cli/plugins/plugin.command.js';
24+
import { RemovePluginQuestionSet } from '@app/unraid-api/cli/plugins/remove-plugin.questions.js';
2425
import { PM2Service } from '@app/unraid-api/cli/pm2.service.js';
2526
import { ReportCommand } from '@app/unraid-api/cli/report.command.js';
2627
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
@@ -72,6 +73,7 @@ const DEFAULT_PROVIDERS = [
7273
DeleteApiKeyQuestionSet,
7374
AddSSOUserQuestionSet,
7475
RemoveSSOUserQuestionSet,
76+
RemovePluginQuestionSet,
7577
DeveloperQuestions,
7678
DeveloperToolsService,
7779
LogService,

0 commit comments

Comments
 (0)