Skip to content

Commit

Permalink
feat: allow user to choose organization in shorebird init (#2514)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanoltman authored Oct 8, 2024
1 parent 78faea7 commit 238837f
Show file tree
Hide file tree
Showing 10 changed files with 481 additions and 34 deletions.
2 changes: 1 addition & 1 deletion packages/artifact_proxy/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -586,4 +586,4 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0-259.0.dev <4.0.0"
dart: ">=3.5.0 <4.0.0"
35 changes: 33 additions & 2 deletions packages/shorebird_cli/lib/src/code_push_client_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ class CodePushClientWrapper {

final CodePushClient codePushClient;

Future<App> createApp({String? appName}) async {
Future<App> createApp({
required int organizationId,
String? appName,
}) async {
late final String displayName;
if (appName == null) {
String? defaultAppName;
Expand All @@ -96,7 +99,35 @@ class CodePushClientWrapper {
displayName = appName;
}

return codePushClient.createApp(displayName: displayName);
return codePushClient.createApp(
displayName: displayName,
organizationId: organizationId,
);
}

/// Returns the currently logged in user, or null if no user is logged in.
Future<PrivateUser?> getCurrentUser() async {
final progress = logger.progress('Fetching user');
try {
final user = await codePushClient.getCurrentUser();
progress.complete();
return user;
} catch (error) {
_handleErrorAndExit(error, progress: progress);
}
}

Future<List<OrganizationMembership>> getOrganizationMemberships() async {
final progress = logger.progress('Fetching organizations');
final List<OrganizationMembership> memberships;
try {
memberships = await codePushClient.getOrganizationMemberships();
progress.complete();
} catch (error) {
_handleErrorAndExit(error, progress: progress);
}

return memberships;
}

Future<List<AppMetadata>> getApps() async {
Expand Down
34 changes: 32 additions & 2 deletions packages/shorebird_cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
import 'package:shorebird_cli/src/extensions/organization.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform/ios.dart';
import 'package:shorebird_cli/src/pubspec_editor.dart';
Expand Down Expand Up @@ -65,6 +66,26 @@ Please make sure you are running "shorebird init" from within your Flutter proje
return ExitCode.software.code;
}

final orgs = await codePushClientWrapper.getOrganizationMemberships();
if (orgs.isEmpty) {
logger.err(
'''You do not have any organizations. This should never happen. Please contact us on Discord or send us an email at contact@shorebird.dev.''',
);
return ExitCode.software.code;
}

final user = await codePushClientWrapper.getCurrentUser();
final Organization organization;
if (orgs.length > 1) {
organization = logger.chooseOne(
'Which organization should this app belong to?',
choices: orgs.map((o) => o.organization).toList(),
display: (o) => o.displayName(user: user!),
);
} else {
organization = orgs.first.organization;
}

final force = results['force'] == true;

Set<String>? androidFlavors;
Expand Down Expand Up @@ -130,6 +151,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
for (final flavor in newFlavors) {
final app = await codePushClientWrapper.createApp(
appName: '$deflavoredAppName ($flavor)',
organizationId: organization.id,
);
flavorsToAppIds[flavor] = app.id;
}
Expand Down Expand Up @@ -171,17 +193,24 @@ Please make sure you are running "shorebird init" from within your Flutter proje
if (hasNoFlavors) {
// No platforms have any flavors so we just create a single app
// and assign it as the default.
final app = await codePushClientWrapper.createApp(appName: displayName);
final app = await codePushClientWrapper.createApp(
appName: displayName,
organizationId: organization.id,
);
appId = app.id;
} else if (hasSomeFlavors) {
// Some platforms have flavors and some do not so we create an app
// for the default (no flavor) and then create an app per flavor.
final app = await codePushClientWrapper.createApp(appName: displayName);
final app = await codePushClientWrapper.createApp(
appName: displayName,
organizationId: organization.id,
);
appId = app.id;
final values = <String, String>{};
for (final flavor in productFlavors) {
final app = await codePushClientWrapper.createApp(
appName: '$displayName ($flavor)',
organizationId: organization.id,
);
values[flavor] = app.id;
}
Expand All @@ -193,6 +222,7 @@ Please make sure you are running "shorebird init" from within your Flutter proje
for (final flavor in productFlavors) {
final app = await codePushClientWrapper.createApp(
appName: '$displayName ($flavor)',
organizationId: organization.id,
);
values[flavor] = app.id;
}
Expand Down
12 changes: 12 additions & 0 deletions packages/shorebird_cli/lib/src/extensions/organization.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';

/// {@template organization_display}
/// Returns the user-facing display name of an [Organization].
/// {@endtemplate}
extension OrganizationDisplay on Organization {
/// {@macro organization_display}
String displayName({required PrivateUser user}) => switch (organizationType) {
OrganizationType.team => name,
OrganizationType.personal => user.displayName ?? user.email,
};
}
109 changes: 103 additions & 6 deletions packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,44 +212,141 @@ void main() {
).thenAnswer((_) async => flutterVersion);
});

group('getCurrentUser', () {
group('when getCurrentUser request fails', () {
setUp(() {
when(
() => codePushClient.getCurrentUser(),
).thenThrow(Exception('something went wrong'));
});

test('exits with code 70', () async {
await expectLater(
() async => runWithOverrides(codePushClientWrapper.getCurrentUser),
exitsWithCode(ExitCode.software),
);
verify(() => progress.fail(any())).called(1);
});
});

group('when getCurrentUser request succeeds', () {
final user = PrivateUser.forTest();
setUp(() {
when(() => codePushClient.getCurrentUser()).thenAnswer(
(_) async => user,
);
});

test('returns current user', () async {
final result = await runWithOverrides(
codePushClientWrapper.getCurrentUser,
);
expect(result, user);
verify(() => progress.complete()).called(1);
});
});
});

group('app', () {
const organizationId = 123;

group('createApp', () {
test('prompts for displayName when not provided', () async {
const appName = 'test app';
const app = App(id: appId, displayName: 'Test App');
when(() => logger.prompt(any())).thenReturn(appName);
when(() => codePushClient.createApp(displayName: appName)).thenAnswer(
when(
() => codePushClient.createApp(
displayName: appName,
organizationId: any(named: 'organizationId'),
),
).thenAnswer(
(_) async => app,
);

await runWithOverrides(
() => codePushClientWrapper.createApp(),
() => codePushClientWrapper.createApp(
organizationId: organizationId,
),
);

verify(() => logger.prompt(any())).called(1);
verify(
() => codePushClient.createApp(displayName: appName),
() => codePushClient.createApp(
displayName: appName,
organizationId: organizationId,
),
).called(1);
});

test('does not prompt for displayName when not provided', () async {
const appName = 'test app';
const app = App(id: appId, displayName: 'Test App');
when(() => codePushClient.createApp(displayName: appName)).thenAnswer(
when(
() => codePushClient.createApp(
displayName: appName,
organizationId: any(named: 'organizationId'),
),
).thenAnswer(
(_) async => app,
);

await runWithOverrides(
() => codePushClientWrapper.createApp(appName: appName),
() => codePushClientWrapper.createApp(
appName: appName,
organizationId: organizationId,
),
);

verifyNever(() => logger.prompt(any()));
verify(
() => codePushClient.createApp(displayName: appName),
() => codePushClient.createApp(
displayName: appName,
organizationId: organizationId,
),
).called(1);
});
});

group('getOrganizationMemberships', () {
test('exits with code 70 when getting organization memberships fails',
() async {
const error = 'something went wrong';
when(() => codePushClient.getOrganizationMemberships())
.thenThrow(error);

await expectLater(
() async => runWithOverrides(
codePushClientWrapper.getOrganizationMemberships,
),
exitsWithCode(ExitCode.software),
);
verify(() => progress.fail(error)).called(1);
});

test('returns organization memberships on success', () async {
final expectedMemberships = [
OrganizationMembership(
organization: Organization.forTest(),
role: OrganizationRole.admin,
),
OrganizationMembership(
organization: Organization.forTest(),
role: OrganizationRole.member,
),
];
when(() => codePushClient.getOrganizationMemberships())
.thenAnswer((_) async => expectedMemberships);

final memberships = await runWithOverrides(
codePushClientWrapper.getOrganizationMemberships,
);

expect(memberships, equals(expectedMemberships));
verify(() => progress.complete()).called(1);
});
});

group('getApps', () {
test('exits with code 70 when getting apps fails', () async {
const error = 'something went wrong';
Expand Down
Loading

0 comments on commit 238837f

Please sign in to comment.