Skip to content

Commit 92c9a9d

Browse files
author
serverpod_cloud
committed
feat(scloud): 3bbfc2ae55db0606a08a57de7362b4da8be9114d
1 parent 8237da0 commit 92c9a9d

5 files changed

Lines changed: 264 additions & 4 deletions

File tree

serverpod_cloud_cli/lib/command_runner/commands/admin/admin_projects_commands.dart

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ class AdminProjectCommand extends CloudCliCommand {
1010
final name = 'project';
1111

1212
@override
13-
final description = 'Inspect Serverpod Cloud projects.';
13+
final description = 'Manage Serverpod Cloud projects.';
1414

1515
AdminProjectCommand({required super.logger}) {
1616
addSubcommand(AdminListProjectsCommand(logger: logger));
1717
addSubcommand(AdminProjectStatusCommand(logger: logger));
18+
addSubcommand(AdminProjectDeleteCommand(logger: logger));
1819
}
1920
}
2021

@@ -118,3 +119,47 @@ class AdminProjectStatusCommand
118119
table.writeLines(logger.line);
119120
}
120121
}
122+
123+
enum AdminProjectDeleteOption<V> implements OptionDefinition<V> {
124+
projectId(
125+
StringOption(
126+
argName: 'project',
127+
argAbbrev: 'p',
128+
argPos: 0,
129+
mandatory: true,
130+
helpText:
131+
'The ID of the project. '
132+
'Can be passed as the first argument.',
133+
),
134+
);
135+
136+
const AdminProjectDeleteOption(this.option);
137+
138+
@override
139+
final ConfigOptionBase<V> option;
140+
}
141+
142+
class AdminProjectDeleteCommand
143+
extends CloudCliCommand<AdminProjectDeleteOption> {
144+
@override
145+
final name = 'delete';
146+
147+
@override
148+
final description = 'Delete a Serverpod Cloud project.';
149+
150+
AdminProjectDeleteCommand({required super.logger})
151+
: super(options: AdminProjectDeleteOption.values);
152+
153+
@override
154+
Future<void> runWithConfig(
155+
final Configuration<AdminProjectDeleteOption> commandConfig,
156+
) async {
157+
final projectId = commandConfig.value(AdminProjectDeleteOption.projectId);
158+
159+
await ProjectAdminCommands.deleteProject(
160+
runner.serviceProvider.cloudApiClient,
161+
logger: logger,
162+
projectId: projectId,
163+
);
164+
}
165+
}

serverpod_cloud_cli/lib/command_runner/completion/completion_script_carapace.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ commands:
106106
- name: link
107107
flags:
108108
-p, --project=!: "The ID of the project. Can be passed as the first argument."
109+
--dart-version=: "Overrides the Dart SDK version to use for building the project."
109110
110111
- name: user
111112
@@ -131,6 +132,7 @@ commands:
131132
--dry-run: "Do not actually deploy, just print the deployment steps."
132133
--show-files: "Display the file tree that will be uploaded."
133134
-o, --output=: "Save the deployment zip file to the specified path. Must end with .zip"
135+
--dart-version=: "Overrides the Dart SDK version to use for building the project."
134136
135137
- name: variable
136138
@@ -309,6 +311,7 @@ commands:
309311
--no-enable-db: "Flag to enable the database for the project."
310312
--deploy: "Flag to immediately deploy the project."
311313
--no-deploy: "Flag to immediately deploy the project."
314+
--dart-version=: "Overrides the Dart SDK version to use for building the project."
312315
exclusiveFlags:
313316
- [enable-db, no-enable-db]
314317
- [deploy, no-deploy]

serverpod_cloud_cli/lib/command_runner/completion/completion_script_completely.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ _scloud_completions() {
769769
;;
770770
771771
'project link'*)
772-
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project -p")" -- "$cur")
772+
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project -p --dart-version")" -- "$cur")
773773
;;
774774
775775
'project user'*)
@@ -889,15 +889,15 @@ _scloud_completions() {
889889
;;
890890
891891
'deploy'*)
892-
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project -p --concurrency -c --dry-run --show-files --output -o")" -- "$cur")
892+
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project -p --concurrency -c --dry-run --show-files --output -o --dart-version")" -- "$cur")
893893
;;
894894
895895
'log'*'-d')
896896
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -- "$cur")
897897
;;
898898
899899
'launch'*)
900-
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project --new-project --enable-db --no-enable-db --deploy --no-deploy")" -- "$cur")
900+
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_scloud_completions_filter "--quiet -q --verbose -v --analytics --no-analytics -a --version --token --project-dir -d --project-config-file --timeout --yes --project --new-project --enable-db --no-enable-db --deploy --no-deploy --dart-version")" -- "$cur")
901901
;;
902902
903903
'domain'*)

serverpod_cloud_cli/lib/commands/admin/project_admin.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ abstract class ProjectAdminCommands {
5858
);
5959
}
6060

61+
static Future<void> deleteProject(
62+
final Client cloudApiClient, {
63+
required final CommandLogger logger,
64+
required final String projectId,
65+
}) async {
66+
final shouldDelete = await logger.confirm(
67+
'Are you sure you want to delete the project "$projectId"?',
68+
defaultValue: false,
69+
);
70+
71+
if (!shouldDelete) {
72+
throw UserAbortException();
73+
}
74+
75+
try {
76+
await cloudApiClient.adminProjects.deleteProject(
77+
cloudProjectId: projectId,
78+
);
79+
} on Exception catch (e, s) {
80+
throw FailureException.nested(
81+
e,
82+
s,
83+
'Request to delete the project failed',
84+
);
85+
}
86+
87+
logger.success('Deleted the project "$projectId".', newParagraph: true);
88+
}
89+
6190
static String _formatProjectUsers(final Project project) {
6291
return project.roles
6392
?.map((final r) {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import 'dart:async';
2+
3+
import 'package:ground_control_client/ground_control_client.dart';
4+
import 'package:ground_control_client/ground_control_client_test_tools.dart';
5+
import 'package:mocktail/mocktail.dart';
6+
import 'package:serverpod_cloud_cli/command_runner/cloud_cli_command_runner.dart';
7+
import 'package:serverpod_cloud_cli/command_runner/commands/admin/admin_projects_commands.dart';
8+
import 'package:serverpod_cloud_cli/command_runner/helpers/cloud_cli_service_provider.dart';
9+
import 'package:serverpod_cloud_cli/shared/exceptions/exit_exceptions.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../../../test_utils/command_logger_matchers.dart';
13+
import '../../../test_utils/test_command_logger.dart';
14+
15+
void main() {
16+
final logger = TestCommandLogger();
17+
final client = ClientMock(
18+
authKeyProvider: InMemoryKeyManager.authenticated(),
19+
);
20+
final cli = CloudCliCommandRunner.create(
21+
logger: logger,
22+
serviceProvider: CloudCliServiceProvider(
23+
apiClientFactory: (final globalCfg) => client,
24+
),
25+
adminUserMode: true,
26+
);
27+
28+
tearDown(() async {
29+
reset(client.adminProjects);
30+
logger.clear();
31+
});
32+
33+
const projectId = 'my-proj';
34+
35+
test(
36+
'Given admin project delete command when instantiated then requires login',
37+
() {
38+
expect(AdminProjectDeleteCommand(logger: logger).requireLogin, isTrue);
39+
},
40+
);
41+
42+
group('Given authenticated', () {
43+
group('when executing admin project delete and accepting the prompt', () {
44+
late Future commandResult;
45+
setUp(() async {
46+
when(
47+
() => client.adminProjects.deleteProject(
48+
cloudProjectId: any(named: 'cloudProjectId'),
49+
),
50+
).thenAnswer(
51+
(final invocation) async =>
52+
ProjectBuilder().withCloudProjectId(projectId).build(),
53+
);
54+
55+
logger.answerNextConfirmWith(true);
56+
57+
commandResult = cli.run(['admin', 'project', 'delete', projectId]);
58+
});
59+
60+
test('then command completes successfully', () async {
61+
await expectLater(commandResult, completes);
62+
verify(
63+
() => client.adminProjects.deleteProject(cloudProjectId: projectId),
64+
).called(1);
65+
});
66+
67+
test('then command logs confirm message', () async {
68+
await commandResult;
69+
70+
expect(logger.confirmCalls, isNotEmpty);
71+
expect(
72+
logger.confirmCalls.first,
73+
equalsConfirmCall(
74+
message: 'Are you sure you want to delete the project "my-proj"?',
75+
defaultValue: false,
76+
),
77+
);
78+
});
79+
80+
test('then command outputs success message', () async {
81+
await commandResult;
82+
83+
expect(logger.successCalls, isNotEmpty);
84+
expect(
85+
logger.successCalls.first,
86+
equalsSuccessCall(
87+
message: 'Deleted the project "my-proj".',
88+
newParagraph: true,
89+
),
90+
);
91+
});
92+
});
93+
94+
group('when executing admin project delete and rejecting the prompt', () {
95+
late Future commandResult;
96+
setUp(() async {
97+
when(
98+
() => client.adminProjects.deleteProject(
99+
cloudProjectId: any(named: 'cloudProjectId'),
100+
),
101+
).thenAnswer(
102+
(final invocation) async =>
103+
ProjectBuilder().withCloudProjectId(projectId).build(),
104+
);
105+
106+
logger.answerNextConfirmWith(false);
107+
108+
commandResult = cli.run(['admin', 'project', 'delete', projectId]);
109+
});
110+
111+
test('then command throws exit exception', () async {
112+
await expectLater(commandResult, throwsA(isA<ErrorExitException>()));
113+
});
114+
115+
test('then logs confirm message', () async {
116+
try {
117+
await commandResult;
118+
} catch (_) {}
119+
120+
expect(logger.confirmCalls, isNotEmpty);
121+
expect(
122+
logger.confirmCalls.first,
123+
equalsConfirmCall(
124+
message: 'Are you sure you want to delete the project "my-proj"?',
125+
defaultValue: false,
126+
),
127+
);
128+
});
129+
130+
test('then logs no success message', () async {
131+
try {
132+
await commandResult;
133+
} catch (_) {}
134+
135+
expect(logger.successCalls, isEmpty);
136+
});
137+
138+
test('then deleteProject is not called', () async {
139+
try {
140+
await commandResult;
141+
} catch (_) {}
142+
143+
verifyNever(
144+
() => client.adminProjects.deleteProject(cloudProjectId: projectId),
145+
);
146+
});
147+
});
148+
149+
group('when executing admin project delete and API returns not found', () {
150+
late Future commandResult;
151+
setUp(() async {
152+
when(
153+
() => client.adminProjects.deleteProject(
154+
cloudProjectId: any(named: 'cloudProjectId'),
155+
),
156+
).thenThrow(NotFoundException(message: 'No such project: $projectId'));
157+
158+
logger.answerNextConfirmWith(true);
159+
160+
commandResult = cli.run(['admin', 'project', 'delete', projectId]);
161+
});
162+
163+
test('then command throws exit exception', () async {
164+
await expectLater(commandResult, throwsA(isA<ErrorExitException>()));
165+
});
166+
167+
test('then logs error message', () async {
168+
try {
169+
await commandResult;
170+
} catch (_) {}
171+
172+
expect(logger.errorCalls, isNotEmpty);
173+
expect(
174+
logger.errorCalls.first,
175+
equalsErrorCall(
176+
message: 'The requested resource did not exist.',
177+
hint: 'No such project: my-proj',
178+
),
179+
);
180+
});
181+
});
182+
});
183+
}

0 commit comments

Comments
 (0)