diff --git a/packages/cli/src/lib/Cli.ts b/packages/cli/src/lib/Cli.ts index 42a95fa6..d2777657 100644 --- a/packages/cli/src/lib/Cli.ts +++ b/packages/cli/src/lib/Cli.ts @@ -28,6 +28,7 @@ import {BUBBLEWRAP_LOGO} from './constants'; import {updateConfig} from './cmds/updateConfig'; import {doctor} from './cmds/doctor'; import {merge} from './cmds/merge'; +import {fingerprint} from './cmds/fingerprint'; export class Cli { async run(args: string[]): Promise { @@ -79,6 +80,8 @@ export class Cli { return await doctor(); case 'merge': return await merge(parsedArgs); + case 'fingerprint': + return await fingerprint(parsedArgs); default: throw new Error( `"${command}" is not a valid command! Use 'bubblewrap help' for a list of commands`); diff --git a/packages/cli/src/lib/cmds/build.ts b/packages/cli/src/lib/cmds/build.ts index b65c8eac..6c027d89 100644 --- a/packages/cli/src/lib/cmds/build.ts +++ b/packages/cli/src/lib/cmds/build.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import {AndroidSdkTools, Config, DigitalAssetLinks, GradleWrapper, JdkHelper, KeyTool, Log, +import {AndroidSdkTools, Config, GradleWrapper, JdkHelper, KeyTool, Log, ConsoleLog, TwaManifest, JarSigner, SigningKeyInfo, Result} from '@bubblewrap/core'; import * as path from 'path'; -import * as fs from 'fs'; import {enUS as messages} from '../strings'; import {Prompt, InquirerPrompt} from '../Prompt'; import {PwaValidator, PwaValidationResult} from '@bubblewrap/validator'; import {printValidationResult} from '../pwaValidationHelper'; import {ParsedArgs} from 'minimist'; import {createValidateString} from '../inputHelpers'; +import {TWA_MANIFEST_FILE_NAME} from '../constants'; // Path to the file generated when building an app bundle file using gradle. const APP_BUNDLE_BUILD_OUTPUT_FILE_NAME = './app/build/outputs/bundle/release/app-release.aab'; @@ -38,9 +38,6 @@ const APK_SIGNED_FILE_NAME = './app-release-signed.apk'; // Output file for zipalign. const APK_ALIGNED_FILE_NAME = './app-release-unsigned-aligned.apk'; -const TWA_MANIFEST_FILE_NAME = './twa-manifest.json'; -const ASSETLINKS_OUTPUT_FILE = './assetlinks.json'; - interface SigningKeyPasswords { keystorePassword: string; keyPassword: string; @@ -103,34 +100,6 @@ class Build { } } - async generateAssetLinks( - twaManifest: TwaManifest, passwords: SigningKeyPasswords): Promise { - try { - const digitalAssetLinksFile = ASSETLINKS_OUTPUT_FILE; - const keyInfo = await this.keyTool.keyInfo({ - path: twaManifest.signingKey.path, - alias: twaManifest.signingKey.alias, - keypassword: passwords.keyPassword, - password: passwords.keystorePassword, - }); - - const sha256Fingerprint = keyInfo.fingerprints.get('SHA256'); - if (!sha256Fingerprint) { - this.prompt.printMessage(messages.messageSha256FingerprintNotFound); - return; - } - - const digitalAssetLinks = - DigitalAssetLinks.generateAssetLinks(twaManifest.packageId, sha256Fingerprint); - - await fs.promises.writeFile(digitalAssetLinksFile, digitalAssetLinks); - - this.prompt.printMessage(messages.messageDigitalAssetLinksSuccess(digitalAssetLinksFile)); - } catch (e) { - this.prompt.printMessage(messages.errorAssetLinksGeneration); - } - } - async buildApk(): Promise { await this.gradleWrapper.assembleRelease(); await this.androidSdkTools.zipalign( @@ -199,10 +168,6 @@ class Build { APP_BUNDLE_SIGNED_FILE_NAME; this.prompt.printMessage(messages.messageAppBundleSuccess(appBundleFileName)); - if (passwords) { - await this.generateAssetLinks(twaManifest, passwords); - } - if (validationPromise !== null) { const result = await validationPromise; if (result.isOk()) { diff --git a/packages/cli/src/lib/cmds/fingerprint.ts b/packages/cli/src/lib/cmds/fingerprint.ts new file mode 100644 index 00000000..8ecfdc61 --- /dev/null +++ b/packages/cli/src/lib/cmds/fingerprint.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2021 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 {ParsedArgs} from 'minimist'; +import {TwaManifest, DigitalAssetLinks, Fingerprint} from '@bubblewrap/core'; +import {TWA_MANIFEST_FILE_NAME, ASSETLINKS_OUTPUT_FILE} from '../constants'; +import {Prompt, InquirerPrompt} from '../Prompt'; +import * as path from 'path'; +import * as fs from 'fs'; +import {enUS} from '../strings'; +import {validateSha256Fingerprint} from '../inputHelpers'; + +async function loadManifest(args: ParsedArgs, prompt: Prompt): Promise { + const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME); + prompt.printMessage(enUS.messageLoadingTwaManifestFrom(manifestFile)); + if (!fs.existsSync(manifestFile)) { + throw new Error(enUS.errorCouldNotfindTwaManifest(manifestFile)); + } + return await TwaManifest.fromFile(manifestFile); +} + +async function saveManifest( + args: ParsedArgs, twaManifest: TwaManifest, prompt: Prompt): Promise { + const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME); + prompt.printMessage(enUS.messageSavingTwaManifestTo(manifestFile)); + await twaManifest.saveToFile(manifestFile); +} + +async function generateAssetLinks( + args: ParsedArgs, prompt: Prompt, twaManifest?: TwaManifest): Promise { + twaManifest = twaManifest || await loadManifest(args, prompt); + const fingerprints = twaManifest.fingerprints.map((value) => value.value); + const digitalAssetLinks = + DigitalAssetLinks.generateAssetLinks(twaManifest.packageId, ...fingerprints); + const digitalAssetLinksFile = args.output || path.join(process.cwd(), ASSETLINKS_OUTPUT_FILE); + await fs.promises.writeFile(digitalAssetLinksFile, digitalAssetLinks); + prompt.printMessage(enUS.messageGeneratedAssetLinksFile(digitalAssetLinksFile)); + return true; +} + +async function addFingerprint(args: ParsedArgs, prompt: Prompt): Promise { + if (args._.length < 3) { + throw new Error(enUS.errorMissingArgument(3, args._.length)); + } + const fingerprintValue = (await validateSha256Fingerprint(args._[2])).unwrap(); + const twaManifest = await loadManifest(args, prompt); + const fingerprint: Fingerprint = {name: args.name, value: fingerprintValue}; + twaManifest.fingerprints.push(fingerprint); + prompt.printMessage(enUS.messageAddedFingerprint(fingerprint)); + await saveManifest(args, twaManifest, prompt); + return await generateAssetLinks(args, prompt, twaManifest); +} + +async function removeFingerprint(args: ParsedArgs, prompt: Prompt): Promise { + if (args._.length < 3) { + throw new Error(enUS.errorMissingArgument(3, args._.length)); + } + const fingerprint = args._[2]; + const twaManifest = await loadManifest(args, prompt); + twaManifest.fingerprints = + twaManifest.fingerprints.filter((value) => { + if (value.value === fingerprint) { + prompt.printMessage(enUS.messageRemovedFingerprint(value)); + return false; + } + return true; + }); + await saveManifest(args, twaManifest, prompt); + return await generateAssetLinks(args, prompt, twaManifest); +} + +async function listFingerprints(args: ParsedArgs, prompt: Prompt): Promise { + const twaManifest = await loadManifest(args, prompt); + twaManifest.fingerprints.forEach((fingerprint) => { + console.log(`\t${fingerprint.name || ''}: ${fingerprint.value}`); + }); + return true; +} + +export async function fingerprint( + args: ParsedArgs, + prompt: Prompt = new InquirerPrompt()): Promise { + if (args._.length < 2) { + throw new Error(enUS.errorMissingArgument(2, args._.length)); + } + const subcommand = args._[1]; + switch (subcommand) { + case 'add': + return await addFingerprint(args, prompt); + case 'remove': + return await removeFingerprint(args, prompt); + case 'list': + return await listFingerprints(args, prompt); + case 'generateAssetLinks': + return await generateAssetLinks(args, prompt); + default: + throw new Error(`Unknown subcommand: ${subcommand}`); + } +} diff --git a/packages/cli/src/lib/cmds/help.ts b/packages/cli/src/lib/cmds/help.ts index 20b21013..813d9080 100644 --- a/packages/cli/src/lib/cmds/help.ts +++ b/packages/cli/src/lib/cmds/help.ts @@ -35,6 +35,7 @@ const HELP_MESSAGES = new Map( 'doctor .............. checks that the jdk and the androidSdk are in place and at the' + ' correct version', 'merge ............... merges your web manifest into twaManifest.json', + 'fingerprint ......... generates the assetlinks.json file and manages keys', ].join('\n')], ['init', [ 'Usage:', @@ -121,6 +122,42 @@ const HELP_MESSAGES = new Map( '--ignore [fields-list]................. the fields which you would like to keep the same.', 'You can enter each key from your Web Manifest.', ].join('\n')], + ['fingerprint', [ + 'Usage:', + '', + '', + 'bubblewrap fingerprint [subcommand]', + '', + ' Global fingerprint flags: ', + ' --manifest= ........ Path to the Trusted Web Activity configuration.', + '', + '', + ' - add: adds a fingerprint to the project configuration.', + ' Usage:', + ' bubblewrap fingerprint add [SHA-256 fingerprint] ', + '', + ' Flags:', + ' --name= ...... a name for the fingerprint', + '', + '', + ' - remove: removes a fingerprint from the project configuration.', + ' Usage:', + ' bubblewrap fingerprint remove [SHA-256 fingerprint] ', + '', + '', + ' - list: lists the fingerprints in the project configuration', + ' Usage:', + ' bubblewrap fingerprint list ', + '', + '', + ' - generateAssetLinks: Generates an AssetLinks file from the project configuration.', + ' Usage:', + ' bubblewrap generateAssetLinks ', + '', + ' Flags:', + ' --output= .... path from where to load the project configuration.', + '', + ].join('\n')], ], ); diff --git a/packages/cli/src/lib/constants.ts b/packages/cli/src/lib/constants.ts index b38de857..d8f10abe 100644 --- a/packages/cli/src/lib/constants.ts +++ b/packages/cli/src/lib/constants.ts @@ -17,6 +17,7 @@ import {magenta} from 'colors'; export const APP_NAME = 'bubblewrap-cli'; +export const ASSETLINKS_OUTPUT_FILE = './assetlinks.json'; export const BUBBLEWRAP_LOGO = magenta( /* eslint-disable indent */ `,-----. ,--. ,--. ,--. @@ -26,3 +27,4 @@ export const BUBBLEWRAP_LOGO = magenta( \`------' \`----' \`---' \`---'\`--'\`----'--' '--\`--' \`--\`--| |-' \`--\' `); /* eslint-enable indent */ +export const TWA_MANIFEST_FILE_NAME = './twa-manifest.json'; diff --git a/packages/cli/src/lib/inputHelpers.ts b/packages/cli/src/lib/inputHelpers.ts index 0fef8c57..e8d58e89 100644 --- a/packages/cli/src/lib/inputHelpers.ts +++ b/packages/cli/src/lib/inputHelpers.ts @@ -254,3 +254,18 @@ export async function validatePackageId(input: string): Promise} a result that resolves to a {@link string} on + * success or {@link Error} on failure. + */ +export async function validateSha256Fingerprint(input: string): Promise> { + input = input.toUpperCase(); + if (input.match(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/)) { + return Result.ok(input); + } + return Result.error(new Error(messages.errorInvalidSha256Fingerprint(input))); +} diff --git a/packages/cli/src/lib/strings.ts b/packages/cli/src/lib/strings.ts index d9708e8b..31d60c37 100644 --- a/packages/cli/src/lib/strings.ts +++ b/packages/cli/src/lib/strings.ts @@ -14,25 +14,30 @@ * limitations under the License. */ +import {Fingerprint} from '@bubblewrap/core'; import {cyan, green, underline, bold, italic, red, yellow} from 'colors'; type Messages = { errorAssetLinksGeneration: string; + errorCouldNotfindTwaManifest: (file: string) => string; errorDirectoryDoesNotExist: (directory: string) => string; errorFailedToRunQualityCriteria: string; errorMaxLength: (maxLength: number, actualLength: number) => string; errorMinLength: (minLength: number, actualLength: number) => string; errorMissingManifestParameter: string; + errorMissingArgument: (expected: number, received: number) => string; errorRequireHttps: string; errorInvalidUrl: (url: string) => string; errorInvalidColor: (color: string) => string; errorInvalidDisplayMode: (displayMode: string) => string; errorInvalidOrientation: (orientation: string) => string; errorInvalidInteger: (integer: string) => string; + errorInvalidSha256Fingerprint: (fingerprint: string) => string; errorUrlMustBeImage: (mimeType: string) => string; errorUrlMustNotBeSvg: string; errorSdkTerms: string; messageInitializingWebManifest: (manifestUrl: string) => string; + messageAddedFingerprint: (fingerpring: Fingerprint) => string; messageAndroidAppDetails: string; messageAndroidAppDetailsDesc: string; messageApkSuccess: (filename: string) => string; @@ -40,14 +45,18 @@ type Messages = { messageBuildingApp: string; messageDigitalAssetLinksSuccess: (filename: string) => string; messageEnterPasswords: (keypath: string, keyalias: string) => string; + messageGeneratedAssetLinksFile: (outputfile: string) => string; messageGeneratedNewVersion: (appVersionName: string, appVersionCode: number) => string; messageGeneratingAndroidProject: string; messageInstallingBuildTools: string; messageLauncherIconAndSplash: string; messageLauncherIconAndSplashDesc: string; + messageLoadingTwaManifestFrom: (path: string) => string; messageOptionFeatures: string; messageOptionalFeaturesDesc: string; messageProjectGeneratedSuccess: string; + messageRemovedFingerprint: (fingerpring: Fingerprint) => string; + messageSavingTwaManifestTo: (path: string) => string; messageSha256FingerprintNotFound: string; messageSigningKeyCreation: string; messageSigningKeyInformation: string; @@ -105,6 +114,9 @@ type Messages = { export const enUS: Messages = { errorAssetLinksGeneration: 'Error generating "assetlinks.json"', + errorCouldNotfindTwaManifest: (file: string): string => { + return `Could not load a manifest from: ${cyan(file)}.`; + }, errorDirectoryDoesNotExist: (directory: string): string => { return `Cannot write to directory: ${directory}.`; }, @@ -116,6 +128,10 @@ export const enUS: Messages = { errorMinLength: (minLength, actualLength): string => { return `Minimum length is ${minLength} but input is ${actualLength}.`; }, + errorMissingArgument: (expected: number, received: number): string => { + return `Expected ${cyan(expected.toString())} arguments \ +but received ${cyan(received.toString())}. Run ${cyan('bubblewrap help')} for usage.`; + }, errorMissingManifestParameter: `Missing required parameter ${cyan('--manifest')}`, errorRequireHttps: 'Url must be https.', errorInvalidUrl: (url: string): string => { @@ -133,11 +149,17 @@ export const enUS: Messages = { errorInvalidInteger: (integer: string): string => { return `Invalid integer provided: ${integer}`; }, + errorInvalidSha256Fingerprint: (fingerprint: string): string => { + return `Invalid SHA-256 fingerprint ${red(fingerprint)}.`; + }, errorUrlMustBeImage: (mimeType: string): string => { return `URL must resolve to an image/* mime-type, but resolved to ${mimeType}.`; }, errorUrlMustNotBeSvg: 'SVG images are not supported yet.', errorSdkTerms: 'Downloading Android SDK failed because Terms and Conditions was not signed.', + messageAddedFingerprint: (fingerprint: Fingerprint): string => { + return `Added fingerprint with value ${fingerprint.value}.`; + }, messageAndroidAppDetails: underline(`\nAndroid app details ${green('(2/5)')}`), messageAndroidAppDetailsDesc: ` Please, enter details regarding how the Android app will look when installed @@ -182,6 +204,9 @@ into a device: return `Please, enter passwords for the keystore ${cyan(keypath)} and alias \ ${cyan(keyalias)}.\n`; }, + messageGeneratedAssetLinksFile: (outputfile: string): string => { + return `\nGenerated Digital Asset Links file at ${cyan(outputfile)}.`; + }, messageGeneratedNewVersion: (appVersionName: string, appVersionCode: number): string => { return `Generated new version with versionName: ${appVersionName} and ` + `versionCode: ${appVersionCode}`; @@ -210,6 +235,9 @@ a blank white page to users. messageInitializingWebManifest: (manifestUrl: string): string => { return `Initializing application from Web Manifest:\n\t- ${cyan(manifestUrl)}`; }, + messageLoadingTwaManifestFrom: (path: string): string => { + return `Loading TWA Manifest from: ${cyan(path)}`; + }, messageOptionFeatures: underline(`\nOptional Features ${green('(4/5)')}`), messageOptionalFeaturesDesc: ` \t- ${bold('Include app shortcuts:')} This question is only prompted if a @@ -224,6 +252,12 @@ a blank white page to users. \t ${italic('theme_color')}. They will be used for notification icons.\n`, messageProjectGeneratedSuccess: '\nProject generated successfully. Build it by running ' + cyan('bubblewrap build'), + messageRemovedFingerprint: (fingerprint: Fingerprint): string => { + return `Removed fingerprint with value ${fingerprint.value}.`; + }, + messageSavingTwaManifestTo: (path: string): string => { + return `Saving TWA Manifest to: ${cyan(path)}`; + }, messageSha256FingerprintNotFound: 'Could not find SHA256 fingerprint. Skipping generating ' + '"assetlinks.json"', messageSigningKeyCreation: underline('\nSigning key creation'), diff --git a/packages/cli/src/spec/inputHelpersSpec.ts b/packages/cli/src/spec/inputHelpersSpec.ts index d572470a..85ef0f62 100644 --- a/packages/cli/src/spec/inputHelpersSpec.ts +++ b/packages/cli/src/spec/inputHelpersSpec.ts @@ -16,6 +16,9 @@ import * as inputHelpers from '../lib/inputHelpers'; +const VALID_FINGERPRINT = + '5F:7B:3E:88:A1:1E:13:96:88:34:5E:78:41:56:C1:90:75:7D:DB:CE:2E:7D:93:19:40:37:1D:1D:AA:F7:F3:F8'; + describe('inputHelpers', () => { describe('#createValidateString', () => { it('Passes validations without constraints', async () => { @@ -191,4 +194,13 @@ describe('inputHelpers', () => { expect((await inputHelpers.validateHost('a b c')).isError()).toBeTrue(); }); }); + + describe('#validateSha256Fingerprint', () => { + it('Succeeds for valid fingerprints', async () => { + expect((await inputHelpers.validateSha256Fingerprint(VALID_FINGERPRINT)).isOk()).toBeTrue(); + }); + it('Fails for invalid fingerprints', async () => { + expect((await inputHelpers.validateSha256Fingerprint('abc123')).isError()).toBeTrue(); + }); + }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a5d15a30..2f4885f0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,7 +24,7 @@ import {JarSigner} from './lib/jdk/JarSigner'; import {JdkHelper} from './lib/jdk/JdkHelper'; import {KeyTool} from './lib/jdk/KeyTool'; import {TwaManifest, DisplayModes, DisplayMode, asDisplayMode, Orientation, Orientations, - asOrientation, SigningKeyInfo} from './lib/TwaManifest'; + asOrientation, SigningKeyInfo, Fingerprint} from './lib/TwaManifest'; import {TwaGenerator} from './lib/TwaGenerator'; import {DigitalAssetLinks} from './lib/DigitalAssetLinks'; import * as util from './lib/util'; @@ -35,6 +35,7 @@ export { BufferedLog, Config, DigitalAssetLinks, + Fingerprint, GradleWrapper, JarSigner, JdkHelper, diff --git a/packages/core/src/lib/DigitalAssetLinks.ts b/packages/core/src/lib/DigitalAssetLinks.ts index 7b674d96..d6953c8e 100644 --- a/packages/core/src/lib/DigitalAssetLinks.ts +++ b/packages/core/src/lib/DigitalAssetLinks.ts @@ -15,11 +15,20 @@ */ export class DigitalAssetLinks { - static generateAssetLinks(applicationId: string, sha256Fingerprint: string): string { - return `[{ - "relation": ["delegate_permission/common.handle_all_urls"], - "target" : { "namespace": "android_app", "package_name": "${applicationId}", - "sha256_cert_fingerprints": ["${sha256Fingerprint}"] } - }]\n`; + static generateAssetLinks(applicationId: string, ...sha256Fingerprints: string[]): string { + const assetlinks = new Array(); + assetlinks.push('['); + sha256Fingerprints.forEach((sha256Fingerprint, index) => { + if (index > 0) { + assetlinks.push(','); + } + assetlinks.push(`{ + "relation": ["delegate_permission/common.handle_all_urls"], + "target" : { "namespace": "android_app", "package_name": "${applicationId}", + "sha256_cert_fingerprints": ["${sha256Fingerprint}"] } + }\n`); + }); + assetlinks.push(']'); + return assetlinks.join(''); } } diff --git a/packages/core/src/lib/TwaManifest.ts b/packages/core/src/lib/TwaManifest.ts index 99718b05..cea16a94 100644 --- a/packages/core/src/lib/TwaManifest.ts +++ b/packages/core/src/lib/TwaManifest.ts @@ -153,6 +153,7 @@ export class TwaManifest { isChromeOSOnly: boolean; shareTarget?: ShareTarget; orientation: Orientation; + fingerprints: Fingerprint[]; private static log = new ConsoleLog('twa-manifest'); @@ -195,6 +196,7 @@ export class TwaManifest { this.isChromeOSOnly = data.isChromeOSOnly != undefined ? data.isChromeOSOnly : false; this.shareTarget = data.shareTarget; this.orientation = data.orientation || DEFAULT_ORIENTATION; + this.fingerprints = data.fingerprints || []; } /** @@ -221,7 +223,6 @@ export class TwaManifest { * @param {String} filename the location where the TWA Manifest will be saved. */ async saveToFile(filename: string): Promise { - console.log('Saving Config to: ' + filename); const json: TwaManifestJson = this.toJson(); await fs.promises.writeFile(filename, JSON.stringify(json, null, 2)); } @@ -518,9 +519,15 @@ export interface TwaManifestJson { isChromeOSOnly?: boolean; shareTarget?: ShareTarget; orientation?: Orientation; + fingerprints?: Fingerprint[]; } export interface SigningKeyInfo { path: string; alias: string; } + +export type Fingerprint = { + name?: string; + value: string; +} diff --git a/packages/core/src/spec/lib/DigitalAssetLinksSpec.ts b/packages/core/src/spec/lib/DigitalAssetLinksSpec.ts index 63b94b19..de04d661 100644 --- a/packages/core/src/spec/lib/DigitalAssetLinksSpec.ts +++ b/packages/core/src/spec/lib/DigitalAssetLinksSpec.ts @@ -16,10 +16,11 @@ import {DigitalAssetLinks} from '../../lib/DigitalAssetLinks'; +const packageName = 'com.test.twa'; + describe('DigitalAssetLinks', () => { describe('#generateAssetLinks', () => { it('Generates the assetlinks markup', () => { - const packageName = 'com.test.twa'; const fingerprint = 'FINGERPRINT'; const digitalAssetLinks = JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, fingerprint)); @@ -31,5 +32,27 @@ describe('DigitalAssetLinks', () => { expect(digitalAssetLinks[0].target.sha256_cert_fingerprints.length).toBe(1); expect(digitalAssetLinks[0].target.sha256_cert_fingerprints[0]).toBe(fingerprint); }); + + it('Generates empty assetlinks.json', () => { + const digitalAssetLinks = + JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, ...new Array())); + expect(digitalAssetLinks.length).toBe(0); + }); + + it('Supports multiple fingerprints', () => { + const digitalAssetLinks = + JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, '123', '456')); + expect(digitalAssetLinks.length).toBe(2); + expect(digitalAssetLinks[0].relation[0]).toBe('delegate_permission/common.handle_all_urls'); + expect(digitalAssetLinks[0].target.namespace).toBe('android_app'); + expect(digitalAssetLinks[0].target.package_name).toBe(packageName); + expect(digitalAssetLinks[0].target.sha256_cert_fingerprints.length).toBe(1); + expect(digitalAssetLinks[0].target.sha256_cert_fingerprints[0]).toBe('123'); + expect(digitalAssetLinks[1].relation[0]).toBe('delegate_permission/common.handle_all_urls'); + expect(digitalAssetLinks[1].target.namespace).toBe('android_app'); + expect(digitalAssetLinks[1].target.package_name).toBe(packageName); + expect(digitalAssetLinks[1].target.sha256_cert_fingerprints.length).toBe(1); + expect(digitalAssetLinks[1].target.sha256_cert_fingerprints[0]).toBe('456'); + }); }); });