From a40acc24baae9946832d6eec50043e6d14997f8e Mon Sep 17 00:00:00 2001 From: Andre Bandarra Date: Thu, 9 Apr 2020 18:22:02 +0100 Subject: [PATCH 1/3] Implements input validation for the AplicationId - Implements validation and tests - Improves some error messages on the CLI --- packages/cli/src/lib/cmds/init.ts | 52 ++++++++++++++++++++++---- packages/core/src/index.ts | 2 + packages/core/src/lib/util.ts | 31 +++++++++++++++ packages/core/src/spec/lib/utilSpec.ts | 30 +++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/cmds/init.ts b/packages/cli/src/lib/cmds/init.ts index 374603b3..6134ae11 100644 --- a/packages/cli/src/lib/cmds/init.ts +++ b/packages/cli/src/lib/cmds/init.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import Color = require('color'); import * as inquirer from 'inquirer'; -import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest} from '@bubblewrap/core'; +import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, util} from '@bubblewrap/core'; import {validateColor, validatePassword, validateUrl, notEmpty} from '../inputHelpers'; import {ParsedArgs} from 'minimist'; import {APP_NAME} from '../constants'; @@ -36,19 +36,34 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Domain being opened in the TWA:', default: twaManifest.host, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('host cannot be empty'); + }, }, { name: 'name', type: 'input', message: 'Name of the application:', default: twaManifest.name, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('name cannot be empty'); + }, }, { name: 'launcherName', type: 'input', message: 'Name to be shown on the Android Launcher:', default: twaManifest.launcherName, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('Launcher name cannot be empty'); + }, }, { name: 'themeColor', type: 'input', @@ -66,7 +81,12 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Relative path to open the TWA:', default: twaManifest.startUrl, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('URL cannot be empty'); + }, }, { name: 'iconUrl', type: 'input', @@ -91,19 +111,35 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Android Package Name (or Application ID):', default: twaManifest.packageId, - validate: notEmpty, + validate: async (input): Promise => { + if (!util.validatePackageId(input)) { + throw new Error('Invalid Application Id. Check requiements at ' + + 'https://developer.android.com/studio/build/application-id'); + } + return true; + }, }, { name: 'keyPath', type: 'input', message: 'Location of the Signing Key:', default: twaManifest.signingKey.path, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('KeyStore location cannot be empty'); + }, }, { name: 'keyAlias', type: 'input', message: 'Key name:', default: twaManifest.signingKey.alias, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('Key alias cannot be empty'); + }, }, ]); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 28b5377b..29ac8f5f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,6 +22,7 @@ import {JdkHelper} from './lib/jdk/JdkHelper'; import {KeyTool} from './lib/jdk/KeyTool'; import {TwaManifest} from './lib/TwaManifest'; import {TwaGenerator} from './lib/TwaGenerator'; +import * as util from './lib/util'; export {AndroidSdkTools, Config, @@ -31,4 +32,5 @@ export {AndroidSdkTools, Log, TwaGenerator, TwaManifest, + util, }; diff --git a/packages/core/src/lib/util.ts b/packages/core/src/lib/util.ts index e9e2df5f..5a6bc97a 100644 --- a/packages/core/src/lib/util.ts +++ b/packages/core/src/lib/util.ts @@ -28,6 +28,7 @@ const extractZipPromise = promisify(extractZip); // Regex for disallowed characters on Android Packages, as per // https://developer.android.com/guide/topics/manifest/manifest-element.html#package const DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX = /[^a-zA-Z0-9_\.]/g; +const VALID_PACKAGE_ID_SEGMENT_REGEX = /^[a-zA-Z][A-Za-z0-9_]*$/; export async function execute(cmd: string[], env: NodeJS.ProcessEnv): Promise { await execPromise(cmd.join(' '), {env: env}); @@ -134,3 +135,33 @@ export function generatePackageId(host: string): string { parts.push('twa'); return parts.join('.').replace(DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX, '_'); } + +/** + * Validates a Package Id, according to the documentation at: + * https://developer.android.com/studio/build/application-id + * + * Rules summary for the Package Id: + * - It must have at least two segments (one or more dots). + * - Each segment must start with a leter. + * - All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + * + * @param {string} input the package name to be validated + */ +export function validatePackageId(input: string): boolean { + if (input.length <= 0) { + return false; + } + + const parts = input.split('.'); + if (parts.length < 2) { + return false; + } + + for (const part of parts) { + if (part.match(VALID_PACKAGE_ID_SEGMENT_REGEX) === null) { + return false; + } + } + + return true; +} diff --git a/packages/core/src/spec/lib/utilSpec.ts b/packages/core/src/spec/lib/utilSpec.ts index f709ab77..d4b3af1b 100644 --- a/packages/core/src/spec/lib/utilSpec.ts +++ b/packages/core/src/spec/lib/utilSpec.ts @@ -137,4 +137,34 @@ describe('util', () => { expect(result).toBe('com.appspot.pwa_directory_test.twa'); }); }); + + describe('#validatePackageId', () => { + it('returns true for valid packages', () => { + expect(util.validatePackageId('com.pwa_directory.appspot.com')).toBeTrue(); + expect(util.validatePackageId('com.pwa1directory.appspot.com')).toBeTrue(); + }); + + it('returns false for packages with invalid characters', () => { + expect(util.validatePackageId('com.pwa-directory.appspot.com')).toBeFalse(); + expect(util.validatePackageId('com.pwa@directory.appspot.com')).toBeFalse(); + expect(util.validatePackageId('com.pwa*directory.appspot.com')).toBeFalse(); + }); + + it('returns false for packages empty sections', () => { + expect(util.validatePackageId('com.example.')).toBeFalse(); + expect(util.validatePackageId('.com.example')).toBeFalse(); + expect(util.validatePackageId('com..example')).toBeFalse(); + }); + + it('packages with less than 2 sections return false', () => { + expect(util.validatePackageId('com')).toBeFalse(); + expect(util.validatePackageId('')).toBeFalse(); + }); + + it('packages starting with non-letters return false', () => { + expect(util.validatePackageId('com.1char.twa')).toBeFalse(); + expect(util.validatePackageId('1com.char.twa')).toBeFalse(); + expect(util.validatePackageId('com.char.1twa')).toBeFalse(); + }); + }); }); From f372ca86c5ef6ee6c161c61923e1d542507629a9 Mon Sep 17 00:00:00 2001 From: Andre Bandarra Date: Wed, 15 Apr 2020 12:14:34 +0100 Subject: [PATCH 2/3] Improves input validation - Removes repetitions for notEmpty validations. - Improves validation for keystore passwords. - Adds tests for notEmpty, validateKeyPassword and validatecolor. --- packages/cli/package-lock.json | 6 +++ packages/cli/package.json | 5 +- packages/cli/src/lib/cmds/build.ts | 6 +-- packages/cli/src/lib/cmds/init.ts | 65 ++++++++--------------- packages/cli/src/lib/cmds/update.ts | 3 +- packages/cli/src/lib/inputHelpers.ts | 27 +++++++--- packages/cli/src/spec/inputHelpersSpec.ts | 64 ++++++++++++++++++++++ 7 files changed, 119 insertions(+), 57 deletions(-) create mode 100644 packages/cli/src/spec/inputHelpersSpec.ts diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index f0a42c51..b94dac54 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -34,6 +34,12 @@ "rxjs": "^6.4.0" } }, + "@types/jasmine": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.10.tgz", + "integrity": "sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew==", + "dev": true + }, "@types/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index a6e47f67..d72180bd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "tsc", "lint": "eslint \"src/**/*.{js,ts}\"", - "test": "" + "test": "tsc && jasmine --config=jasmine.json" }, "files": [ "dist", @@ -21,6 +21,9 @@ }, "author": "", "license": "Apache-2.0", + "devDependencies": { + "@types/jasmine": "^3.5.0" + }, "dependencies": { "@bubblewrap/core": "^0.6.0", "@bubblewrap/validator": "^0.6.0", diff --git a/packages/cli/src/lib/cmds/build.ts b/packages/cli/src/lib/cmds/build.ts index de8d91c1..8229b963 100644 --- a/packages/cli/src/lib/cmds/build.ts +++ b/packages/cli/src/lib/cmds/build.ts @@ -18,7 +18,7 @@ import {AndroidSdkTools, Config, GradleWrapper, JdkHelper, Log, TwaManifest} from '@bubblewrap/core'; import * as inquirer from 'inquirer'; import * as path from 'path'; -import {validatePassword} from '../inputHelpers'; +import {validateKeyPassword} from '../inputHelpers'; import {PwaValidator, PwaValidationResult} from '@bubblewrap/validator'; import {printValidationResult} from '../pwaValidationHelper'; import {ParsedArgs} from 'minimist'; @@ -55,13 +55,13 @@ async function getPasswords(log: Log): Promise { name: 'password', type: 'password', message: 'KeyStore password:', - validate: validatePassword, + validate: validateKeyPassword, mask: '*', }, { name: 'keypassword', type: 'password', message: 'Key password:', - validate: validatePassword, + validate: validateKeyPassword, mask: '*', }, ]); diff --git a/packages/cli/src/lib/cmds/init.ts b/packages/cli/src/lib/cmds/init.ts index 6134ae11..e724e063 100644 --- a/packages/cli/src/lib/cmds/init.ts +++ b/packages/cli/src/lib/cmds/init.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import Color = require('color'); import * as inquirer from 'inquirer'; import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, util} from '@bubblewrap/core'; -import {validateColor, validatePassword, validateUrl, notEmpty} from '../inputHelpers'; +import {validateColor, validateKeyPassword, validateUrl, notEmpty} from '../inputHelpers'; import {ParsedArgs} from 'minimist'; import {APP_NAME} from '../constants'; @@ -36,34 +36,19 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Domain being opened in the TWA:', default: twaManifest.host, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('host cannot be empty'); - }, + validate: async (input): Promise => notEmpty(input, 'host cannot be empty'), }, { name: 'name', type: 'input', message: 'Name of the application:', default: twaManifest.name, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('name cannot be empty'); - }, + validate: async (input): Promise => notEmpty(input, 'name cannot be empty'), }, { name: 'launcherName', type: 'input', message: 'Name to be shown on the Android Launcher:', default: twaManifest.launcherName, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('Launcher name cannot be empty'); - }, + validate: async (input): Promise => notEmpty(input, 'Launcher name cannot be empty'), }, { name: 'themeColor', type: 'input', @@ -81,12 +66,7 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Relative path to open the TWA:', default: twaManifest.startUrl, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('URL cannot be empty'); - }, + validate: async (input): Promise => notEmpty(input, 'URL cannot be empty'), }, { name: 'iconUrl', type: 'input', @@ -100,7 +80,7 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise 'maskable icons', default: twaManifest.maskableIconUrl, filter: (input): string | undefined => input.length === 0 ? undefined : input, - validate: (input): boolean => input === undefined || validateUrl(input), + validate: async (input): Promise => input === undefined || await validateUrl(input), }, { name: 'shortcuts', type: 'confirm', @@ -123,23 +103,14 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Location of the Signing Key:', default: twaManifest.signingKey.path, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('KeyStore location cannot be empty'); - }, + validate: async (input): Promise => + notEmpty(input, 'KeyStore location cannot be empty'), }, { name: 'keyAlias', type: 'input', message: 'Key name:', default: twaManifest.signingKey.alias, - validate: async (input): Promise => { - if (notEmpty(input)) { - return true; - } - throw new Error('Key alias cannot be empty'); - }, + validate: async (input): Promise => notEmpty(input, 'Key alias cannot be empty'), }, ]); @@ -190,32 +161,36 @@ async function createSigningKey(twaManifest: TwaManifest, config: Config): Promi name: 'fullName', type: 'input', message: 'First and Last names (eg: John Doe):', - validate: notEmpty, + validate: async (input): Promise => + notEmpty(input, 'First and Last names can\'t be empty'), }, { name: 'organizationalUnit', type: 'input', message: 'Organizational Unit (eg: Engineering Dept):', - validate: notEmpty, + validate: async (input): Promise => + notEmpty(input, 'Organizational Unit can\'t be empty'), }, { name: 'organization', type: 'input', message: 'Organization (eg: Company Name):', - validate: notEmpty, + validate: async (input): Promise => + notEmpty(input, 'Organization can\'t be empty'), }, { name: 'country', type: 'input', message: 'Country (2 letter code):', - validate: notEmpty, + validate: async (input): Promise => + notEmpty(input, 'Country can\'t be empty'), }, { name: 'password', type: 'password', message: 'Password for the Key Store:', - validate: validatePassword, + validate: validateKeyPassword, }, { name: 'keypassword', type: 'password', message: 'Password for the Key:', - validate: validatePassword, + validate: validateKeyPassword, }, ]); @@ -240,5 +215,7 @@ export async function init(args: ParsedArgs, config: Config): Promise { await twaManifest.saveToFile('./twa-manifest.json'); await twaGenerator.createTwaProject(targetDirectory, twaManifest); await createSigningKey(twaManifest, config); + log.info(''); + log.info('Project generated successfully. Build it by running "@bubblewrap/cli build"'); return true; } diff --git a/packages/cli/src/lib/cmds/update.ts b/packages/cli/src/lib/cmds/update.ts index 341a266f..c8c1d40e 100644 --- a/packages/cli/src/lib/cmds/update.ts +++ b/packages/cli/src/lib/cmds/update.ts @@ -52,7 +52,8 @@ async function updateVersions(twaManifest: TwaManifest, appVersionNameArg: strin type: 'input', message: 'versionName for the new App version:', default: twaManifest.appVersionName, - validate: notEmpty, + validate: async (input): Promise => + notEmpty(input, 'versionName can\'t be empty'), }]); return { diff --git a/packages/cli/src/lib/inputHelpers.ts b/packages/cli/src/lib/inputHelpers.ts index 25132403..268871be 100644 --- a/packages/cli/src/lib/inputHelpers.ts +++ b/packages/cli/src/lib/inputHelpers.ts @@ -17,23 +17,34 @@ import Color = require('color'); import {isWebUri} from 'valid-url'; -export function validatePassword(input: string): boolean { - return input.length > 0; +const MIN_KEY_PASSWORD_LENGTH = 6; + +export async function validateKeyPassword(input: string): Promise { + if (input.trim().length < MIN_KEY_PASSWORD_LENGTH) { + throw new Error(`Password must be at least ${MIN_KEY_PASSWORD_LENGTH} characters long`); + } + return true; } -export function notEmpty(input: string): boolean { - return input.trim().length > 0; +export async function notEmpty(input: string, errorMessage: string): Promise { + if (input.trim().length > 0) { + return true; + } + throw new Error(errorMessage); } -export function validateColor(color: string): boolean { +export async function validateColor(color: string): Promise { try { new Color(color); return true; } catch (_) { - return false; + throw new Error(`Invalid Color ${color}. Try using hexadecimal representation. eg: #FF3300`); } }; -export function validateUrl(url: string): boolean { - return isWebUri(url) !== undefined; +export async function validateUrl(url: string): Promise { + if (isWebUri(url) === undefined) { + throw new Error(`${url} is not an URL`); + } + return true; } diff --git a/packages/cli/src/spec/inputHelpersSpec.ts b/packages/cli/src/spec/inputHelpersSpec.ts new file mode 100644 index 00000000..6f556c23 --- /dev/null +++ b/packages/cli/src/spec/inputHelpersSpec.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as inputHelpers from '../lib/inputHelpers'; + +describe('inputHelpers', () => { + describe('#notEmpty', () => { + it('throws Error for empty strings', async () => { + await expectAsync(inputHelpers.notEmpty('', 'Error')).toBeRejectedWithError(); + await expectAsync(inputHelpers.notEmpty(' ', 'Error')).toBeRejectedWithError(); + }); + + it('returns true for non-empty input', async () => { + expect(await inputHelpers.notEmpty('a', 'Error')).toBeTrue(); + }); + }); + + describe('#validateKeyPassword', () => { + it('throws Error for empty string', async () => { + await expectAsync(inputHelpers.validateKeyPassword('')).toBeRejectedWithError(); + await expectAsync(inputHelpers.validateKeyPassword(' ')).toBeRejectedWithError(); + }); + + it('throws Error for empty string', async () => { + await expectAsync(inputHelpers.validateKeyPassword('a')).toBeRejectedWithError(); + await expectAsync(inputHelpers.validateKeyPassword('abc')).toBeRejectedWithError(); + await expectAsync(inputHelpers.validateKeyPassword('abcde')).toBeRejectedWithError(); + await expectAsync(inputHelpers.validateKeyPassword('abcde ')).toBeRejectedWithError(); + }); + + it('throws Error for empty string', async () => { + expect(await inputHelpers.validateKeyPassword('abcdef')).toBeTrue(); + expect(await inputHelpers.validateKeyPassword('abcdef ')).toBeTrue(); + }); + }); + + describe('#validateColor', () => { + it('returns true for valid colors', async () => { + expect(await inputHelpers.validateColor('#FF0033')); + expect(await inputHelpers.validateColor('blue')); + expect(await inputHelpers.validateColor('rgb(255, 0, 30)')); + }); + + it('throws Error for invalid colors', async () => { + await expectAsync(inputHelpers.validateColor('')).toBeRejectedWithError(); + await expectAsync(inputHelpers.validateColor('abc')).toBeRejectedWithError(); + await expectAsync( + inputHelpers.validateColor('rgb(23, 0 30')).toBeRejectedWithError(); + }); + }); +}); From 9f0211d0aca3c6c497990eb98b3a9bcd251b81ea Mon Sep 17 00:00:00 2001 From: Andre Bandarra Date: Wed, 15 Apr 2020 15:06:36 +0100 Subject: [PATCH 3/3] Address review notes --- packages/cli/src/lib/cmds/init.ts | 24 ++++++++++------------- packages/cli/src/lib/cmds/update.ts | 2 +- packages/cli/src/lib/inputHelpers.ts | 4 ++-- packages/cli/src/spec/inputHelpersSpec.ts | 4 ++-- packages/core/src/spec/lib/utilSpec.ts | 2 ++ 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/lib/cmds/init.ts b/packages/cli/src/lib/cmds/init.ts index e724e063..261777a0 100644 --- a/packages/cli/src/lib/cmds/init.ts +++ b/packages/cli/src/lib/cmds/init.ts @@ -36,19 +36,19 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Domain being opened in the TWA:', default: twaManifest.host, - validate: async (input): Promise => notEmpty(input, 'host cannot be empty'), + validate: async (input): Promise => notEmpty(input, 'host'), }, { name: 'name', type: 'input', message: 'Name of the application:', default: twaManifest.name, - validate: async (input): Promise => notEmpty(input, 'name cannot be empty'), + validate: async (input): Promise => notEmpty(input, 'name'), }, { name: 'launcherName', type: 'input', message: 'Name to be shown on the Android Launcher:', default: twaManifest.launcherName, - validate: async (input): Promise => notEmpty(input, 'Launcher name cannot be empty'), + validate: async (input): Promise => notEmpty(input, 'Launcher name'), }, { name: 'themeColor', type: 'input', @@ -66,7 +66,7 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Relative path to open the TWA:', default: twaManifest.startUrl, - validate: async (input): Promise => notEmpty(input, 'URL cannot be empty'), + validate: async (input): Promise => notEmpty(input, 'URL'), }, { name: 'iconUrl', type: 'input', @@ -104,13 +104,13 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise message: 'Location of the Signing Key:', default: twaManifest.signingKey.path, validate: async (input): Promise => - notEmpty(input, 'KeyStore location cannot be empty'), + notEmpty(input, 'KeyStore location'), }, { name: 'keyAlias', type: 'input', message: 'Key name:', default: twaManifest.signingKey.alias, - validate: async (input): Promise => notEmpty(input, 'Key alias cannot be empty'), + validate: async (input): Promise => notEmpty(input, 'Key alias'), }, ]); @@ -161,26 +161,22 @@ async function createSigningKey(twaManifest: TwaManifest, config: Config): Promi name: 'fullName', type: 'input', message: 'First and Last names (eg: John Doe):', - validate: async (input): Promise => - notEmpty(input, 'First and Last names can\'t be empty'), + validate: async (input): Promise => notEmpty(input, 'First and Last names'), }, { name: 'organizationalUnit', type: 'input', message: 'Organizational Unit (eg: Engineering Dept):', - validate: async (input): Promise => - notEmpty(input, 'Organizational Unit can\'t be empty'), + validate: async (input): Promise => notEmpty(input, 'Organizational Unit'), }, { name: 'organization', type: 'input', message: 'Organization (eg: Company Name):', - validate: async (input): Promise => - notEmpty(input, 'Organization can\'t be empty'), + validate: async (input): Promise => notEmpty(input, 'Organization'), }, { name: 'country', type: 'input', message: 'Country (2 letter code):', - validate: async (input): Promise => - notEmpty(input, 'Country can\'t be empty'), + validate: async (input): Promise => notEmpty(input, 'Country'), }, { name: 'password', type: 'password', diff --git a/packages/cli/src/lib/cmds/update.ts b/packages/cli/src/lib/cmds/update.ts index c8c1d40e..55648ba5 100644 --- a/packages/cli/src/lib/cmds/update.ts +++ b/packages/cli/src/lib/cmds/update.ts @@ -53,7 +53,7 @@ async function updateVersions(twaManifest: TwaManifest, appVersionNameArg: strin message: 'versionName for the new App version:', default: twaManifest.appVersionName, validate: async (input): Promise => - notEmpty(input, 'versionName can\'t be empty'), + notEmpty(input, 'versionName'), }]); return { diff --git a/packages/cli/src/lib/inputHelpers.ts b/packages/cli/src/lib/inputHelpers.ts index 268871be..3cdf6f3c 100644 --- a/packages/cli/src/lib/inputHelpers.ts +++ b/packages/cli/src/lib/inputHelpers.ts @@ -26,11 +26,11 @@ export async function validateKeyPassword(input: string): Promise { return true; } -export async function notEmpty(input: string, errorMessage: string): Promise { +export async function notEmpty(input: string, fieldName: string): Promise { if (input.trim().length > 0) { return true; } - throw new Error(errorMessage); + throw new Error(`${fieldName} cannot be empty`); } export async function validateColor(color: string): Promise { diff --git a/packages/cli/src/spec/inputHelpersSpec.ts b/packages/cli/src/spec/inputHelpersSpec.ts index 6f556c23..b9ed2207 100644 --- a/packages/cli/src/spec/inputHelpersSpec.ts +++ b/packages/cli/src/spec/inputHelpersSpec.ts @@ -34,14 +34,14 @@ describe('inputHelpers', () => { await expectAsync(inputHelpers.validateKeyPassword(' ')).toBeRejectedWithError(); }); - it('throws Error for empty string', async () => { + it('throws Error input with less than 6 characters', async () => { await expectAsync(inputHelpers.validateKeyPassword('a')).toBeRejectedWithError(); await expectAsync(inputHelpers.validateKeyPassword('abc')).toBeRejectedWithError(); await expectAsync(inputHelpers.validateKeyPassword('abcde')).toBeRejectedWithError(); await expectAsync(inputHelpers.validateKeyPassword('abcde ')).toBeRejectedWithError(); }); - it('throws Error for empty string', async () => { + it('returns true for valid input', async () => { expect(await inputHelpers.validateKeyPassword('abcdef')).toBeTrue(); expect(await inputHelpers.validateKeyPassword('abcdef ')).toBeTrue(); }); diff --git a/packages/core/src/spec/lib/utilSpec.ts b/packages/core/src/spec/lib/utilSpec.ts index d4b3af1b..7a781e1c 100644 --- a/packages/core/src/spec/lib/utilSpec.ts +++ b/packages/core/src/spec/lib/utilSpec.ts @@ -148,6 +148,7 @@ describe('util', () => { expect(util.validatePackageId('com.pwa-directory.appspot.com')).toBeFalse(); expect(util.validatePackageId('com.pwa@directory.appspot.com')).toBeFalse(); expect(util.validatePackageId('com.pwa*directory.appspot.com')).toBeFalse(); + expect(util.validatePackageId('com․pwa-directory.appspot.com')).toBeFalse(); }); it('returns false for packages empty sections', () => { @@ -165,6 +166,7 @@ describe('util', () => { expect(util.validatePackageId('com.1char.twa')).toBeFalse(); expect(util.validatePackageId('1com.char.twa')).toBeFalse(); expect(util.validatePackageId('com.char.1twa')).toBeFalse(); + expect(util.validatePackageId('_com.char.1twa')).toBeFalse(); }); }); });