Skip to content

Commit ce55b32

Browse files
author
serverpod_cloud
committed
feat: bf96d5a1ad9d297a59ecda452eed7cf711a7951c
1 parent af00f1d commit ce55b32

6 files changed

Lines changed: 229 additions & 7 deletions

File tree

serverpod_cloud_cli/lib/command_runner/cloud_cli_command_runner.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:serverpod_cloud_cli/command_runner/commands/log_command.dart';
1616
import 'package:serverpod_cloud_cli/command_runner/commands/project_command.dart';
1717
import 'package:serverpod_cloud_cli/command_runner/commands/secret_command.dart';
1818
import 'package:serverpod_cloud_cli/command_runner/commands/status_command.dart';
19+
import 'package:serverpod_cloud_cli/command_runner/commands/user_command.dart';
1920
import 'package:serverpod_cloud_cli/command_runner/commands/version_command.dart';
2021
import 'package:serverpod_cloud_cli/shared/exceptions/exit_exceptions.dart';
2122
import 'package:serverpod_cloud_cli/command_runner/helpers/cloud_cli_service_provider.dart';
@@ -107,6 +108,7 @@ class CloudCliCommandRunner extends BetterCommandRunner<GlobalOption, void> {
107108
CloudSecretCommand(logger: logger),
108109
CloudDbCommand(logger: logger),
109110
CloudLaunchCommand(logger: logger),
111+
CloudUserCommand(logger: logger),
110112
]);
111113

112114
return runner;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:cli_tools/config.dart';
2+
import 'package:serverpod_cloud_cli/command_runner/cloud_cli_command.dart';
3+
import 'package:serverpod_cloud_cli/command_runner/helpers/command_options.dart';
4+
import 'package:serverpod_cloud_cli/commands/user/user.dart';
5+
6+
import 'categories.dart';
7+
8+
class CloudUserCommand extends CloudCliCommand {
9+
@override
10+
final name = 'user';
11+
12+
@override
13+
final description = 'Manage Serverpod Cloud users.';
14+
15+
@override
16+
String get category => CommandCategories.manage;
17+
18+
CloudUserCommand({required super.logger}) {
19+
addSubcommand(UserListCommand(logger: logger));
20+
}
21+
}
22+
23+
enum ListUsersOption<V> implements OptionDefinition<V> {
24+
projectId(ProjectIdOption());
25+
26+
const ListUsersOption(this.option);
27+
28+
@override
29+
final ConfigOptionBase<V> option;
30+
}
31+
32+
class UserListCommand extends CloudCliCommand<ListUsersOption> {
33+
@override
34+
final name = 'list';
35+
36+
@override
37+
final description = 'List Serverpod Cloud users.';
38+
39+
UserListCommand({required super.logger})
40+
: super(options: ListUsersOption.values);
41+
42+
@override
43+
Future<void> runWithConfig(
44+
final Configuration<ListUsersOption> commandConfig,
45+
) async {
46+
final projectId = commandConfig.value(ListUsersOption.projectId);
47+
48+
await UserCommands.listUsers(
49+
runner.serviceProvider.cloudApiClient,
50+
logger: logger,
51+
projectId: projectId,
52+
);
53+
}
54+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:ground_control_client/ground_control_client.dart';
2+
import 'package:serverpod_cloud_cli/command_logger/command_logger.dart';
3+
import 'package:serverpod_cloud_cli/util/printers/table_printer.dart';
4+
5+
abstract class UserCommands {
6+
static Future<void> listUsers(
7+
final Client cloudApiClient, {
8+
required final CommandLogger logger,
9+
required final String projectId,
10+
}) async {
11+
final users = await cloudApiClient.users.listUsersInProject(
12+
cloudProjectId: projectId,
13+
);
14+
15+
final table = TablePrinter(
16+
headers: ['User', 'Project', 'Project roles'],
17+
rows: users.map((final user) => [
18+
user.email,
19+
projectId,
20+
user.memberships
21+
?.map((final m) => m.role?.name)
22+
.nonNulls
23+
.join(', ') ??
24+
'',
25+
]),
26+
);
27+
table.writeLines(logger.line);
28+
}
29+
}

serverpod_cloud_cli/test_integration/commands/auth/login_command_test.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ void main() {
119119
'--scloud-dir',
120120
testCacheFolderPath
121121
]);
122-
await tokenSent.future;
123122
});
124123

125124
tearDown(() async {
@@ -129,11 +128,16 @@ void main() {
129128
});
130129

131130
test('then cli command completes.', () async {
131+
await tokenSent.future;
132+
132133
await expectLater(cliOnDone, completes);
133134
});
134135

135136
test('then the cloud data with token is stored.', () async {
137+
await tokenSent.future;
138+
136139
await cliOnDone;
140+
137141
final storedCloudData =
138142
await ResourceManager.tryFetchServerpodCloudData(
139143
logger: logger,
@@ -154,7 +158,6 @@ void main() {
154158
'--scloud-dir',
155159
testCacheFolderPath,
156160
]);
157-
await tokenSent.future;
158161
});
159162

160163
tearDown(() async {
@@ -164,11 +167,16 @@ void main() {
164167
});
165168

166169
test('then cli command completes.', () async {
170+
await tokenSent.future;
171+
167172
await expectLater(cliOnDone, completes);
168173
});
169174

170175
test('then no cloud data is stored.', () async {
176+
await tokenSent.future;
177+
171178
await cliOnDone;
179+
172180
final storedCloudData =
173181
await ResourceManager.tryFetchServerpodCloudData(
174182
logger: logger,
@@ -209,7 +217,6 @@ void main() {
209217
'--scloud-dir',
210218
testCacheFolderPath
211219
]);
212-
await tokenSent.future;
213220
});
214221

215222
tearDown(() async {
@@ -219,12 +226,17 @@ void main() {
219226
});
220227

221228
test('then cli command completes throws an exit exception.', () async {
229+
await tokenSent.future;
230+
222231
await expectLater(cliOnDone, throwsA(isA<ErrorExitException>()));
223232
});
224233

225234
test('then no cloud data is stored.', () async {
235+
await tokenSent.future;
236+
226237
// Silence the error message.
227238
await cliOnDone.catchError((final _) {});
239+
228240
final storedCloudData =
229241
await ResourceManager.tryFetchServerpodCloudData(
230242
logger: logger,

serverpod_cloud_cli/test_integration/commands/project_command/invite_project_user_test.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
@Tags(['concurrency_one']) // due to current directory manipulation
2-
library;
3-
41
import 'dart:async';
52

63
import 'package:ground_control_client/ground_control_client.dart'
@@ -18,7 +15,7 @@ import '../../../test_utils/command_logger_matchers.dart';
1815
import '../../../test_utils/test_command_logger.dart';
1916

2017
void main() {
21-
final logger = TestCommandLogger(printToStdout: true);
18+
final logger = TestCommandLogger();
2219
final keyManager = InMemoryKeyManager();
2320
final client = ClientMock(authenticationKeyManager: keyManager);
2421
final cli = CloudCliCommandRunner.create(
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import 'dart:async';
2+
3+
import 'package:ground_control_client/ground_control_client.dart'
4+
show Role, User, UserRoleMembership;
5+
import 'package:mocktail/mocktail.dart';
6+
import 'package:serverpod_cloud_cli/command_runner/commands/user_command.dart';
7+
import 'package:test/test.dart';
8+
9+
import 'package:ground_control_client/ground_control_client_mock.dart';
10+
import 'package:serverpod_cloud_cli/command_runner/cloud_cli_command_runner.dart';
11+
import 'package:serverpod_cloud_cli/shared/exceptions/exit_exceptions.dart';
12+
import 'package:serverpod_cloud_cli/command_runner/helpers/cloud_cli_service_provider.dart';
13+
14+
import '../../../test_utils/command_logger_matchers.dart';
15+
import '../../../test_utils/test_command_logger.dart';
16+
17+
void main() {
18+
final logger = TestCommandLogger();
19+
final keyManager = InMemoryKeyManager();
20+
final client = ClientMock(authenticationKeyManager: keyManager);
21+
final cli = CloudCliCommandRunner.create(
22+
logger: logger,
23+
serviceProvider: CloudCliServiceProvider(
24+
apiClientFactory: (final globalCfg) => client,
25+
),
26+
);
27+
28+
const projectId = 'projectId';
29+
30+
tearDown(() async {
31+
await keyManager.remove();
32+
33+
logger.clear();
34+
});
35+
36+
test('Given user list command when instantiated then requires login', () {
37+
expect(UserListCommand(logger: logger).requireLogin, isTrue);
38+
});
39+
40+
group('Given unauthenticated', () {
41+
group('when executing user list', () {
42+
late Future commandResult;
43+
setUp(() async {
44+
commandResult = cli.run([
45+
'user',
46+
'list',
47+
'--project',
48+
projectId,
49+
]);
50+
});
51+
52+
test('then throws exception', () async {
53+
await expectLater(commandResult, throwsA(isA<ErrorExitException>()));
54+
});
55+
56+
test('then logs error', () async {
57+
await commandResult.catchError((final _) {});
58+
59+
expect(logger.errorCalls, isNotEmpty);
60+
expect(
61+
logger.errorCalls.first,
62+
equalsErrorCall(
63+
message: 'This command requires you to be logged in.',
64+
));
65+
});
66+
});
67+
});
68+
69+
group('Given authenticated', () {
70+
setUp(() async {
71+
await keyManager.put('mock-token');
72+
});
73+
74+
group('when executing user list', () {
75+
late Future commandResult;
76+
setUp(() async {
77+
when(() => client.users.listUsersInProject(
78+
cloudProjectId: any(named: 'cloudProjectId'),
79+
)).thenAnswer(
80+
(final invocation) async => Future.value([
81+
User(
82+
userAuthId: 'userAuthId',
83+
email: 'test@example.com',
84+
memberships: [
85+
UserRoleMembership(
86+
userId: 1,
87+
roleId: 1,
88+
role: Role(
89+
projectId: 1,
90+
name: 'Owners',
91+
projectScopes: [],
92+
),
93+
),
94+
],
95+
),
96+
]),
97+
);
98+
99+
commandResult = cli.run([
100+
'user',
101+
'list',
102+
'--project',
103+
projectId,
104+
]);
105+
});
106+
107+
test('then command completes successfully', () async {
108+
await expectLater(commandResult, completes);
109+
});
110+
111+
test('then command outputs user list', () async {
112+
await commandResult.catchError((final _) {});
113+
114+
expect(
115+
logger.lineCalls,
116+
containsAllInOrder([
117+
equalsLineCall(
118+
line: 'User | Project | Project roles'),
119+
equalsLineCall(
120+
line: '-----------------+-----------+--------------'),
121+
equalsLineCall(
122+
line: 'test@example.com | projectId | Owners '),
123+
]),
124+
);
125+
});
126+
});
127+
});
128+
}

0 commit comments

Comments
 (0)