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

fix: org name validation #151

Merged
merged 8 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions lib/src/commands/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ const _defaultOrgName = 'com.example.verygoodcore';
// capital letters.
// https://dart.dev/guides/language/language-tour#important-concepts
final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
final RegExp _orgNameRegExp =
RegExp(r'[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+');
final RegExp _orgNameRegExp = RegExp(r'^[a-zA-Z][\w-]*(\.[a-zA-Z][\w-]*)+$');

/// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);
Expand Down Expand Up @@ -138,21 +137,29 @@ class CreateCommand extends Command<int> {
}

/// Gets the organization name.
List<String> get _orgName {
if (_argResults['org-name'] == null) return _defaultOrgName.split('.');

final orgName = _argResults['org-name'] as String;
List<Map<String, String>> get _orgName {
final orgName = _argResults['org-name'] as String? ?? _defaultOrgName;
_validateOrgName(orgName);
return orgName.split('.');
final segments = orgName.replaceAll(RegExp(r'-|_'), ' ').split('.');
final org = <Map<String, String>>[];
for (var i = 0; i < segments.length; i++) {
final segment = segments[i];
org.add(
{'value': segment, 'separator': i == segments.length - 1 ? '' : '.'},
);
}
return org;
}

void _validateOrgName(String name) {
final isValidOrgName = _isValidOrgName(name);
if (!isValidOrgName) {
throw UsageException(
'"$name" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'A valid org name has at least 2 parts separated by "."\n'
'Each part must start with a letter and only include '
'alphanumeric characters (A-Z, a-z, 0-9), underscores (_), '
'and hyphens (-)\n'
'(ex. very.good.org)',
usage,
);
Expand All @@ -171,8 +178,7 @@ class CreateCommand extends Command<int> {
}

bool _isValidOrgName(String name) {
final match = _orgNameRegExp.matchAsPrefix(name);
return match != null && match.end == name.length;
return _orgNameRegExp.hasMatch(name);
}

bool _isValidPackageName(String name) {
Expand Down
132 changes: 82 additions & 50 deletions test/src/commands/create_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ void main() {
),
vars: {
'project_name': 'my_app',
'org_name': ['com', 'example', 'verygoodcore'],
'org_name': [
{'value': 'com', 'separator': '.'},
{'value': 'example', 'separator': '.'},
{'value': 'verygoodcore', 'separator': ''}
],
},
),
).called(1);
Expand All @@ -146,47 +150,46 @@ void main() {

group('org-name', () {
group('invalid --org-name', () {
test('no delimiters', () async {
const expectedErrorMessage = '"My App" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
void expectInvalidOrgName(String orgName) async {
final expectedErrorMessage = '"$orgName" is not a valid org name.\n\n'
'A valid org name has at least 2 parts separated by "."\n'
'Each part must start with a letter and only include '
'alphanumeric characters (A-Z, a-z, 0-9), underscores (_), '
'and hyphens (-)\n'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'My App'],
['create', '.', '--org-name', orgName],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
}

test('no delimiters', () async {
expectInvalidOrgName('My App');
});

test('more than 3 domains', () async {
const expectedErrorMessage =
'"very.bad.test.case" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'very.bad.test.case'],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
test('less than 2 domains', () async {
expectInvalidOrgName('verybadtest');
});

test('invalid characters present', () async {
const expectedErrorMessage =
'"very%.bad@.#test" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'very%.bad@.#test'],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
expectInvalidOrgName('very%.bad@.#test');
});

test('segment starts with a non-letter', () async {
expectInvalidOrgName('very.bad.1test');
});

test('valid prefix but invalid suffix', () async {
expectInvalidOrgName('very.good.prefix.bad@@suffix');
});
});

group('valid --org-name', () {
test('completes successfully with correct output', () async {
Future<void> expectValidOrgName(
String orgName,
List<Map<String, String>> expected,
) async {
final argResults = MockArgResults();
final generator = MockMasonGenerator();
final command = CreateCommand(
Expand All @@ -195,7 +198,7 @@ void main() {
generator: (_) async => generator,
)..argResultOverrides = argResults;
when(() => argResults['project-name']).thenReturn('my_app');
when(() => argResults['org-name']).thenReturn('very.good.ventures');
when(() => argResults['org-name']).thenReturn(orgName);
when(() => argResults.rest).thenReturn(['.tmp']);
when(() => generator.id).thenReturn('generator_id');
when(() => generator.description).thenReturn('generator description');
Expand All @@ -204,12 +207,6 @@ void main() {
).thenAnswer((_) async => 62);
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Bootstrapping')).called(1);
expect(progressLogs, equals(['Generated 62 file(s)']));
verify(
() => logger.progress('Running "flutter packages get" in .tmp'),
).called(1);
verify(() => logger.alert('Created a Very Good App! 🦄')).called(1);
verify(
() => generator.generate(
any(
Expand All @@ -219,23 +216,58 @@ void main() {
'.tmp',
),
),
vars: {
'project_name': 'my_app',
'org_name': ['very', 'good', 'ventures'],
},
vars: {'project_name': 'my_app', 'org_name': expected},
),
).called(1);
verify(
() => analytics.sendEvent(
'create',
'generator_id',
label: 'generator description',
),
).called(1);
verify(
() => analytics.waitForLastPing(
timeout: VeryGoodCommandRunner.timeout),
).called(1);
}

test('alphanumeric with three parts', () {
expectValidOrgName('very.good.ventures', [
{'value': 'very', 'separator': '.'},
{'value': 'good', 'separator': '.'},
{'value': 'ventures', 'separator': ''},
]);
});

test('containing an underscore', () {
expectValidOrgName('very.good.test_case', [
{'value': 'very', 'separator': '.'},
{'value': 'good', 'separator': '.'},
{'value': 'test case', 'separator': ''},
]);
});

test('containing a hyphen', () {
expectValidOrgName('very.bad.test-case', [
{'value': 'very', 'separator': '.'},
{'value': 'bad', 'separator': '.'},
{'value': 'test case', 'separator': ''},
]);
});

test('single character parts', () {
expectValidOrgName('v.g.v', [
{'value': 'v', 'separator': '.'},
{'value': 'g', 'separator': '.'},
{'value': 'v', 'separator': ''},
]);
});

test('more than three parts', () {
expectValidOrgName('very.good.ventures.app.identifier', [
{'value': 'very', 'separator': '.'},
{'value': 'good', 'separator': '.'},
{'value': 'ventures', 'separator': '.'},
{'value': 'app', 'separator': '.'},
{'value': 'identifier', 'separator': ''},
]);
});

test('less than three parts', () {
expectValidOrgName('verygood.ventures', [
{'value': 'verygood', 'separator': '.'},
{'value': 'ventures', 'separator': ''},
]);
});
});
});
Expand Down