Skip to content

Commit 65c5ea6

Browse files
authored
fix: org name validation (#151)
1 parent 725ad0f commit 65c5ea6

File tree

2 files changed

+99
-61
lines changed

2 files changed

+99
-61
lines changed

lib/src/commands/create.dart

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ const _defaultOrgName = 'com.example.verygoodcore';
1818
// capital letters.
1919
// https://dart.dev/guides/language/language-tour#important-concepts
2020
final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
21-
final RegExp _orgNameRegExp =
22-
RegExp(r'[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+');
21+
final RegExp _orgNameRegExp = RegExp(r'^[a-zA-Z][\w-]*(\.[a-zA-Z][\w-]*)+$');
2322

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

140139
/// Gets the organization name.
141-
List<String> get _orgName {
142-
if (_argResults['org-name'] == null) return _defaultOrgName.split('.');
143-
144-
final orgName = _argResults['org-name'] as String;
140+
List<Map<String, String>> get _orgName {
141+
final orgName = _argResults['org-name'] as String? ?? _defaultOrgName;
145142
_validateOrgName(orgName);
146-
return orgName.split('.');
143+
final segments = orgName.replaceAll(RegExp(r'-|_'), ' ').split('.');
144+
final org = <Map<String, String>>[];
145+
for (var i = 0; i < segments.length; i++) {
146+
final segment = segments[i];
147+
org.add(
148+
{'value': segment, 'separator': i == segments.length - 1 ? '' : '.'},
149+
);
150+
}
151+
return org;
147152
}
148153

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

173180
bool _isValidOrgName(String name) {
174-
final match = _orgNameRegExp.matchAsPrefix(name);
175-
return match != null && match.end == name.length;
181+
return _orgNameRegExp.hasMatch(name);
176182
}
177183

178184
bool _isValidPackageName(String name) {

test/src/commands/create_test.dart

Lines changed: 82 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ void main() {
128128
),
129129
vars: {
130130
'project_name': 'my_app',
131-
'org_name': ['com', 'example', 'verygoodcore'],
131+
'org_name': [
132+
{'value': 'com', 'separator': '.'},
133+
{'value': 'example', 'separator': '.'},
134+
{'value': 'verygoodcore', 'separator': ''}
135+
],
132136
},
133137
),
134138
).called(1);
@@ -146,47 +150,46 @@ void main() {
146150

147151
group('org-name', () {
148152
group('invalid --org-name', () {
149-
test('no delimiters', () async {
150-
const expectedErrorMessage = '"My App" is not a valid org name.\n\n'
151-
'A valid org name has 3 parts separated by "."'
152-
'and only includes alphanumeric characters and underscores'
153+
void expectInvalidOrgName(String orgName) async {
154+
final expectedErrorMessage = '"$orgName" is not a valid org name.\n\n'
155+
'A valid org name has at least 2 parts separated by "."\n'
156+
'Each part must start with a letter and only include '
157+
'alphanumeric characters (A-Z, a-z, 0-9), underscores (_), '
158+
'and hyphens (-)\n'
153159
'(ex. very.good.org)';
154160
final result = await commandRunner.run(
155-
['create', '.', '--org-name', 'My App'],
161+
['create', '.', '--org-name', orgName],
156162
);
157163
expect(result, equals(ExitCode.usage.code));
158164
verify(() => logger.err(expectedErrorMessage)).called(1);
165+
}
166+
167+
test('no delimiters', () async {
168+
expectInvalidOrgName('My App');
159169
});
160170

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

174175
test('invalid characters present', () async {
175-
const expectedErrorMessage =
176-
'"very%.bad@.#test" is not a valid org name.\n\n'
177-
'A valid org name has 3 parts separated by "."'
178-
'and only includes alphanumeric characters and underscores'
179-
'(ex. very.good.org)';
180-
final result = await commandRunner.run(
181-
['create', '.', '--org-name', 'very%.bad@.#test'],
182-
);
183-
expect(result, equals(ExitCode.usage.code));
184-
verify(() => logger.err(expectedErrorMessage)).called(1);
176+
expectInvalidOrgName('very%.bad@.#test');
177+
});
178+
179+
test('segment starts with a non-letter', () async {
180+
expectInvalidOrgName('very.bad.1test');
181+
});
182+
183+
test('valid prefix but invalid suffix', () async {
184+
expectInvalidOrgName('very.good.prefix.bad@@suffix');
185185
});
186186
});
187187

188188
group('valid --org-name', () {
189-
test('completes successfully with correct output', () async {
189+
Future<void> expectValidOrgName(
190+
String orgName,
191+
List<Map<String, String>> expected,
192+
) async {
190193
final argResults = MockArgResults();
191194
final generator = MockMasonGenerator();
192195
final command = CreateCommand(
@@ -195,7 +198,7 @@ void main() {
195198
generator: (_) async => generator,
196199
)..argResultOverrides = argResults;
197200
when(() => argResults['project-name']).thenReturn('my_app');
198-
when(() => argResults['org-name']).thenReturn('very.good.ventures');
201+
when(() => argResults['org-name']).thenReturn(orgName);
199202
when(() => argResults.rest).thenReturn(['.tmp']);
200203
when(() => generator.id).thenReturn('generator_id');
201204
when(() => generator.description).thenReturn('generator description');
@@ -204,12 +207,6 @@ void main() {
204207
).thenAnswer((_) async => 62);
205208
final result = await command.run();
206209
expect(result, equals(ExitCode.success.code));
207-
verify(() => logger.progress('Bootstrapping')).called(1);
208-
expect(progressLogs, equals(['Generated 62 file(s)']));
209-
verify(
210-
() => logger.progress('Running "flutter packages get" in .tmp'),
211-
).called(1);
212-
verify(() => logger.alert('Created a Very Good App! 🦄')).called(1);
213210
verify(
214211
() => generator.generate(
215212
any(
@@ -219,23 +216,58 @@ void main() {
219216
'.tmp',
220217
),
221218
),
222-
vars: {
223-
'project_name': 'my_app',
224-
'org_name': ['very', 'good', 'ventures'],
225-
},
219+
vars: {'project_name': 'my_app', 'org_name': expected},
226220
),
227221
).called(1);
228-
verify(
229-
() => analytics.sendEvent(
230-
'create',
231-
'generator_id',
232-
label: 'generator description',
233-
),
234-
).called(1);
235-
verify(
236-
() => analytics.waitForLastPing(
237-
timeout: VeryGoodCommandRunner.timeout),
238-
).called(1);
222+
}
223+
224+
test('alphanumeric with three parts', () {
225+
expectValidOrgName('very.good.ventures', [
226+
{'value': 'very', 'separator': '.'},
227+
{'value': 'good', 'separator': '.'},
228+
{'value': 'ventures', 'separator': ''},
229+
]);
230+
});
231+
232+
test('containing an underscore', () {
233+
expectValidOrgName('very.good.test_case', [
234+
{'value': 'very', 'separator': '.'},
235+
{'value': 'good', 'separator': '.'},
236+
{'value': 'test case', 'separator': ''},
237+
]);
238+
});
239+
240+
test('containing a hyphen', () {
241+
expectValidOrgName('very.bad.test-case', [
242+
{'value': 'very', 'separator': '.'},
243+
{'value': 'bad', 'separator': '.'},
244+
{'value': 'test case', 'separator': ''},
245+
]);
246+
});
247+
248+
test('single character parts', () {
249+
expectValidOrgName('v.g.v', [
250+
{'value': 'v', 'separator': '.'},
251+
{'value': 'g', 'separator': '.'},
252+
{'value': 'v', 'separator': ''},
253+
]);
254+
});
255+
256+
test('more than three parts', () {
257+
expectValidOrgName('very.good.ventures.app.identifier', [
258+
{'value': 'very', 'separator': '.'},
259+
{'value': 'good', 'separator': '.'},
260+
{'value': 'ventures', 'separator': '.'},
261+
{'value': 'app', 'separator': '.'},
262+
{'value': 'identifier', 'separator': ''},
263+
]);
264+
});
265+
266+
test('less than three parts', () {
267+
expectValidOrgName('verygood.ventures', [
268+
{'value': 'verygood', 'separator': '.'},
269+
{'value': 'ventures', 'separator': ''},
270+
]);
239271
});
240272
});
241273
});

0 commit comments

Comments
 (0)