Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow user to choose organization in shorebird init #2514

Merged
merged 12 commits into from
Oct 8, 2024
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.''',
bryanoltman marked this conversation as resolved.
Show resolved Hide resolved
);
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
Loading