From 8f42653a8523e1efeea912b269c1f17cabc4e7f1 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Mon, 8 Jul 2024 17:18:37 -0700 Subject: [PATCH 1/9] feat: save identity data to sf's config and to the server --- messages/shared.utils.md | 2 +- src/commands/lightning/preview/app.ts | 18 +++++-- src/configMeta.ts | 8 +-- src/lwc-dev-server/index.ts | 12 ++--- src/shared/configUtils.ts | 74 ++++++++++++++++++++------- test/lwc-dev-server/index.e2e.test.ts | 5 +- test/lwc-dev-server/index.test.ts | 3 +- test/shared/configUtils.test.ts | 57 +++++++++++++-------- 8 files changed, 122 insertions(+), 57 deletions(-) diff --git a/messages/shared.utils.md b/messages/shared.utils.md index 447a82d5..ee9a3f50 100644 --- a/messages/shared.utils.md +++ b/messages/shared.utils.md @@ -16,7 +16,7 @@ Valid workspace value is "SalesforceCLI" OR "mrt" # config-utils.token-desc -The Base64-encoded identity token of the local web server +The identity data is a data structure that ties together a single local web server identity token to multiple orgs. # config-utils.cert-desc diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index ed854de8..72901f77 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -21,6 +21,7 @@ import chalk from 'chalk'; import { OrgUtils } from '../../../shared/orgUtils.js'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; +import { ConfigUtils } from '../../../shared/configUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app'); @@ -157,13 +158,18 @@ export default class LightningPreviewApp extends SfCommand { return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? '']))); } + logger.debug('Configuring local web server identity'); + const connection = targetOrg.getConnection(undefined); + const username = connection.getUsername() as string; + const token = await ConfigUtils.getOrCreateIdentityToken(username, connection); + let appId: string | undefined; if (appName) { logger.debug(`Determining App Id for ${appName}`); // The appName is optional but if the user did provide an appName then it must be // a valid one.... meaning that it should resolve to a valid appId. - appId = await OrgUtils.getAppId(targetOrg.getConnection(undefined), appName); + appId = await OrgUtils.getAppId(connection, appName); if (!appId) { return Promise.reject(new Error(messages.getMessage('error.fetching.app-id', [appName]))); } @@ -180,12 +186,13 @@ export default class LightningPreviewApp extends SfCommand { logger.debug(`Local Dev Server url is ${ldpServerUrl}`); if (platform === Platform.desktop) { - await this.desktopPreview(sfdxProjectRootPath, serverPort, ldpServerUrl, appId, logger); + await this.desktopPreview(sfdxProjectRootPath, serverPort, token, ldpServerUrl, appId, logger); } else { await this.mobilePreview( platform, sfdxProjectRootPath, serverPort, + token, ldpServerUrl, appName, appId, @@ -198,6 +205,7 @@ export default class LightningPreviewApp extends SfCommand { private async desktopPreview( sfdxProjectRootPath: string, serverPort: number, + token: string, ldpServerUrl: string, appId: string | undefined, logger: Logger @@ -233,7 +241,7 @@ export default class LightningPreviewApp extends SfCommand { const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, appId, targetOrg); // Start the LWC Dev Server - await startLWCServer(logger, sfdxProjectRootPath, serverPort); + await startLWCServer(logger, sfdxProjectRootPath, token, serverPort); // Open the browser and navigate to the right page await this.config.runCommand('org:open', launchArguments); @@ -243,6 +251,7 @@ export default class LightningPreviewApp extends SfCommand { platform: Platform.ios | Platform.android, sfdxProjectRootPath: string, serverPort: number, + token: string, ldpServerUrl: string, appName: string | undefined, appId: string | undefined, @@ -316,7 +325,8 @@ export default class LightningPreviewApp extends SfCommand { } // Start the LWC Dev Server - await startLWCServer(logger, sfdxProjectRootPath, serverPort, certData); + + await startLWCServer(logger, sfdxProjectRootPath, token, serverPort, certData); // Launch the native app for previewing (launchMobileApp will show its own spinner) // eslint-disable-next-line camelcase diff --git a/src/configMeta.ts b/src/configMeta.ts index 010b7a81..917ed332 100644 --- a/src/configMeta.ts +++ b/src/configMeta.ts @@ -27,10 +27,10 @@ export type SerializedSSLCertificateData = { export const enum ConfigVars { /** - * The Base64-encoded identity token of the local web server, used to - * validate the web server's identity to the hmr-client. + * The identity data is a data structure that ties together a single + * local web server identity token to multiple orgs. */ - LOCAL_WEB_SERVER_IDENTITY_TOKEN = 'local-web-server-identity-token', + LOCAL_WEB_SERVER_IDENTITY_DATA = 'local-web-server-identity-data', /** * The SSL certificate data to be used by local dev server @@ -50,7 +50,7 @@ export const enum ConfigVars { export default [ { - key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, + key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, description: IDENTITY_TOKEN_DESC, hidden: true, encrypted: true, diff --git a/src/lwc-dev-server/index.ts b/src/lwc-dev-server/index.ts index bccbc1cd..32aa07bb 100644 --- a/src/lwc-dev-server/index.ts +++ b/src/lwc-dev-server/index.ts @@ -46,10 +46,10 @@ function mapLogLevel(cliLogLevel: number): number { async function createLWCServerConfig( logger: Logger, rootDir: string, + token: string, serverPort?: number, certData?: SSLCertificateData, - workspace?: Workspace, - token?: string + workspace?: Workspace ): Promise { const sfdxConfig = path.resolve(rootDir, 'sfdx-project.json'); @@ -83,7 +83,7 @@ async function createLWCServerConfig( paths: namespacePaths, // use custom workspace if any is provided, or fetch from config file (if any), otherwise use the default workspace workspace: workspace ?? (await ConfigUtils.getLocalDevServerWorkspace()) ?? LOCAL_DEV_SERVER_DEFAULT_WORKSPACE, - identityToken: token ?? (await ConfigUtils.getOrCreateIdentityToken()), + identityToken: token, logLevel: mapLogLevel(logger.getLevel()), }; @@ -100,12 +100,12 @@ async function createLWCServerConfig( export async function startLWCServer( logger: Logger, rootDir: string, + token: string, serverPort?: number, certData?: SSLCertificateData, - workspace?: Workspace, - token?: string + workspace?: Workspace ): Promise { - const config = await createLWCServerConfig(logger, rootDir, serverPort, certData, workspace, token); + const config = await createLWCServerConfig(logger, rootDir, token, serverPort, certData, workspace); logger.trace(`Starting LWC Dev Server with config: ${JSON.stringify(config)}`); let lwcDevServer: LWCServer | null = await startLwcDevServer(config); diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 7db49cf4..25ae5f95 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -5,14 +5,27 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + import { Workspace } from '@lwc/lwc-dev-server'; import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; -import { Config, ConfigAggregator } from '@salesforce/core'; +import { Config, ConfigAggregator, Connection } from '@salesforce/core'; import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../configMeta.js'; export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081; export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli; +export class LocalWebServerIdentityData { + public identityToken: string; + public usernameToServerEntityIdMap: Record = {}; + + public constructor(token: string) { + this.identityToken = token; + } +} + export class ConfigUtils { static #config: Config; static #globalConfig: Config; @@ -35,27 +48,29 @@ export class ConfigUtils { return this.#globalConfig; } - public static async getOrCreateIdentityToken(): Promise { - let token = await this.getIdentityToken(); - if (!token) { - token = CryptoUtils.generateIdentityToken(); - await this.writeIdentityToken(token); + public static async getOrCreateIdentityToken(username: string, connection: Connection): Promise { + let identityData = await this.getIdentityData(); + if (!identityData) { + const token = CryptoUtils.generateIdentityToken(); + const entityId = await this.saveIdentityTokenToServer(token, connection); + identityData = new LocalWebServerIdentityData(token); + identityData.usernameToServerEntityIdMap[username] = entityId; + await this.writeIdentityData(identityData); + return token; + } else { + let entityId = identityData.usernameToServerEntityIdMap[username]; + if (!entityId) { + entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection); + identityData.usernameToServerEntityIdMap[username] = entityId; + await this.writeIdentityData(identityData); + } + return identityData.identityToken; } - return token; - } - - public static async getIdentityToken(): Promise { - const config = await ConfigAggregator.create({ customConfigMeta: configMeta }); - // Need to reload to make sure the values read are decrypted - await config.reload(); - const identityToken = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN); - - return identityToken as string; } - public static async writeIdentityToken(token: string): Promise { - const config = await this.getConfig(); - config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, token); + public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise { + const config = await this.getGlobalConfig(); + config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, JSON.stringify(identityData)); await config.write(); } @@ -101,4 +116,25 @@ export class ConfigUtils { return configWorkspace; } + + public static async getIdentityData(): Promise { + const config = await ConfigAggregator.create({ customConfigMeta: configMeta }); + // Need to reload to make sure the values read are decrypted + await config.reload(); + const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA) as string; + + if (identityJson) { + return JSON.parse(identityJson) as LocalWebServerIdentityData; + } + return undefined; + } + + private static async saveIdentityTokenToServer(token: string, connection: Connection): Promise { + const sobject = connection.sobject('UserLocalWebServerIdentity'); + const result = await sobject.insert({ LocalWebServerIdentityToken: token }); + if (result.success) { + return result.id; + } + throw new Error('Could not save the token to the server'); + } } diff --git a/test/lwc-dev-server/index.e2e.test.ts b/test/lwc-dev-server/index.e2e.test.ts index 84cfed75..4c67b32e 100644 --- a/test/lwc-dev-server/index.e2e.test.ts +++ b/test/lwc-dev-server/index.e2e.test.ts @@ -46,8 +46,9 @@ // $$.SANDBOX.resetHistory(); // }); -// it('e2e', async () => { -// const server = await devServer.startLWCServer(logger, path.resolve(__dirname, './__mocks__')); + it('e2e', async () => { +// const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; +// const server = await devServer.startLWCServer(logger, path.resolve(__dirname, './__mocks__'), fakeIdentityToken); // expect(server).to.be.an.instanceOf(LWCServer); // server.stopServer(); diff --git a/test/lwc-dev-server/index.test.ts b/test/lwc-dev-server/index.test.ts index 7495ede7..7606df7c 100644 --- a/test/lwc-dev-server/index.test.ts +++ b/test/lwc-dev-server/index.test.ts @@ -55,7 +55,8 @@ describe('lwc-dev-server', () => { }); it('calling startLWCServer returns an LWCServer', async () => { - const s = await lwcDevServer.startLWCServer(logger, path.resolve(__dirname, './__mocks__')); + const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; + const s = await lwcDevServer.startLWCServer(logger, path.resolve(__dirname, './__mocks__'), fakeIdentityToken); expect(s).to.equal(server); }); }); diff --git a/test/shared/configUtils.test.ts b/test/shared/configUtils.test.ts index b864c964..f5ebf87c 100644 --- a/test/shared/configUtils.test.ts +++ b/test/shared/configUtils.test.ts @@ -5,70 +5,87 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ + + import { expect } from 'chai'; import { Workspace } from '@lwc/lwc-dev-server'; -import { Config, ConfigAggregator } from '@salesforce/core'; +import { Config, ConfigAggregator, Connection } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { CryptoUtils } from '@salesforce/lwc-dev-mobile-core'; -import { ConfigUtils } from '../../src/shared/configUtils.js'; +import { + ConfigUtils, + LOCAL_DEV_SERVER_DEFAULT_PORT, + LOCAL_DEV_SERVER_DEFAULT_WORKSPACE, + LocalWebServerIdentityData, +} from '../../src/shared/configUtils.js'; import { ConfigVars } from '../../src/configMeta.js'; describe('configUtils', () => { const $$ = new TestContext(); + const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; + const username = 'SalesforceDeveloper'; afterEach(() => { $$.restore(); }); - it('getOrCreateIdentityToken resolves if token is found', async () => { - const fakeIdentityToken = 'fake identity token'; - $$.SANDBOX.stub(ConfigUtils, 'getIdentityToken').resolves(fakeIdentityToken); + it('getOrCreateIdentityToken resolves if identity data is found', async () => { + const identityData = new LocalWebServerIdentityData(fakeIdentityToken); + identityData.usernameToServerEntityIdMap[username] = 'entityId'; + const stubConnection = $$.SANDBOX.createStubInstance(Connection); + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(identityData); + $$.SANDBOX.stub(Connection, 'create').resolves(Connection.prototype); + + const resolved = await ConfigUtils.getOrCreateIdentityToken(username, stubConnection); - const resolved = await ConfigUtils.getOrCreateIdentityToken(); expect(resolved).to.equal(fakeIdentityToken); }); - it('getOrCreateIdentityToken resolves and writeIdentityToken is called when there is no token', async () => { - const fakeIdentityToken = 'fake identity token'; - $$.SANDBOX.stub(ConfigUtils, 'getIdentityToken').resolves(undefined); + it('getOrCreateIdentityToken resolves and writeIdentityData is called when there is no identity data', async () => { + const stubConnection = $$.SANDBOX.createStubInstance(Connection); + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(undefined); $$.SANDBOX.stub(CryptoUtils, 'generateIdentityToken').resolves(fakeIdentityToken); - const writeIdentityTokenStub = $$.SANDBOX.stub(ConfigUtils, 'writeIdentityToken').resolves(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $$.SANDBOX.stub(ConfigUtils as any, 'saveIdentityTokenToServer') + .withArgs(fakeIdentityToken, stubConnection) + .resolves('entityId'); + const writeIdentityTokenStub = $$.SANDBOX.stub(ConfigUtils, 'writeIdentityData').resolves(); + + const resolved = await ConfigUtils.getOrCreateIdentityToken(username, stubConnection); - const resolved = await ConfigUtils.getOrCreateIdentityToken(); expect(resolved).to.equal(fakeIdentityToken); expect(writeIdentityTokenStub.calledOnce).to.be.true; }); - it('getIdentityToken resolves to undefined when identity token is not available', async () => { + it('getIdentityData resolves to undefined if identity data is not found', async () => { $$.SANDBOX.stub(ConfigAggregator, 'create').resolves(ConfigAggregator.prototype); $$.SANDBOX.stub(ConfigAggregator.prototype, 'reload').resolves(); $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(undefined); - const resolved = await ConfigUtils.getIdentityToken(); + const resolved = await ConfigUtils.getIdentityData(); expect(resolved).to.equal(undefined); }); - it('getIdentityToken resolves to a string when identity token is available', async () => { - const fakeIdentityToken = 'fake identity token'; + it('getIdentityData resolves when identity data is available', async () => { $$.SANDBOX.stub(ConfigAggregator, 'create').resolves(ConfigAggregator.prototype); $$.SANDBOX.stub(ConfigAggregator.prototype, 'reload').resolves(); $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(fakeIdentityToken); - const resolved = await ConfigUtils.getIdentityToken(); + const resolved = await ConfigUtils.getIdentityData(); expect(resolved).to.equal(fakeIdentityToken); }); - it('writeIdentityToken resolves', async () => { - const fakeIdentityToken = 'fake identity token'; + it('writeIdentityData resolves', async () => { $$.SANDBOX.stub(Config, 'create').withArgs($$.SANDBOX.match.any).resolves(Config.prototype); $$.SANDBOX.stub(Config, 'addAllowedProperties').withArgs($$.SANDBOX.match.any); $$.SANDBOX.stub(Config.prototype, 'set').withArgs( - ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, + ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, $$.SANDBOX.match.string ); $$.SANDBOX.stub(Config.prototype, 'write').resolves(); + const identityData = new LocalWebServerIdentityData(fakeIdentityToken); - const resolved = await ConfigUtils.writeIdentityToken(fakeIdentityToken); + const resolved = await ConfigUtils.writeIdentityData(identityData); expect(resolved).to.equal(undefined); }); From 450b6716f0d9dca43be91672ad8162d278cc9820 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 9 Jul 2024 14:26:23 -0700 Subject: [PATCH 2/9] Update messages/shared.utils.md Co-authored-by: Kevin Hawkins --- messages/shared.utils.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/shared.utils.md b/messages/shared.utils.md index ee9a3f50..143c27f3 100644 --- a/messages/shared.utils.md +++ b/messages/shared.utils.md @@ -16,7 +16,7 @@ Valid workspace value is "SalesforceCLI" OR "mrt" # config-utils.token-desc -The identity data is a data structure that ties together a single local web server identity token to multiple orgs. +The identity data is a data structure that links the local web server's identity token to the user's configured Salesforce orgs. # config-utils.cert-desc From b18bca5781b6e80387f31c4abacea35c1f8c9485 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Tue, 9 Jul 2024 19:12:46 -0700 Subject: [PATCH 3/9] chore: update after code review --- messages/lightning.preview.app.md | 4 +++ messages/shared.utils.md | 2 +- src/commands/lightning/preview/app.ts | 15 ++++++-- src/configMeta.ts | 8 ++--- src/shared/configUtils.ts | 49 +++++++++++++-------------- src/shared/previewUtils.ts | 11 +++++- test/shared/configUtils.test.ts | 13 ++++--- 7 files changed, 63 insertions(+), 39 deletions(-) diff --git a/messages/lightning.preview.app.md b/messages/lightning.preview.app.md index 2b9620fa..364709f1 100644 --- a/messages/lightning.preview.app.md +++ b/messages/lightning.preview.app.md @@ -35,6 +35,10 @@ Type of device to emulate in preview. For mobile virtual devices, specify the device ID to preview. If omitted, the first available virtual device will be used. +# error.username + +Org must have a valid user + # error.no-project This command is required to run from within a Salesforce project directory. %s diff --git a/messages/shared.utils.md b/messages/shared.utils.md index 143c27f3..4063c01b 100644 --- a/messages/shared.utils.md +++ b/messages/shared.utils.md @@ -14,7 +14,7 @@ The workspace name of the local lwc dev server Valid workspace value is "SalesforceCLI" OR "mrt" -# config-utils.token-desc +# config-utils.data-desc The identity data is a data structure that links the local web server's identity token to the user's configured Salesforce orgs. diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index 72901f77..5c2238fa 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -160,7 +160,11 @@ export default class LightningPreviewApp extends SfCommand { logger.debug('Configuring local web server identity'); const connection = targetOrg.getConnection(undefined); - const username = connection.getUsername() as string; + const username = connection.getUsername(); + if (!username) { + return Promise.reject(new Error(messages.getMessage('error.username'))); + } + const token = await ConfigUtils.getOrCreateIdentityToken(username, connection); let appId: string | undefined; @@ -238,7 +242,7 @@ export default class LightningPreviewApp extends SfCommand { this.log(`\n${messages.getMessage('trust.local.dev.server')}`); } - const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, appId, targetOrg); + const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, token, appId, targetOrg); // Start the LWC Dev Server await startLWCServer(logger, sfdxProjectRootPath, token, serverPort); @@ -330,7 +334,12 @@ export default class LightningPreviewApp extends SfCommand { // Launch the native app for previewing (launchMobileApp will show its own spinner) // eslint-disable-next-line camelcase - appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(ldpServerUrl, appName, appId); + appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments( + ldpServerUrl, + token, + appName, + appId + ); await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger); } finally { // stop progress & spinner UX (that may still be running in case of an error) diff --git a/src/configMeta.ts b/src/configMeta.ts index 917ed332..9558ef05 100644 --- a/src/configMeta.ts +++ b/src/configMeta.ts @@ -10,7 +10,7 @@ import { ConfigPropertyMeta, ConfigValue, Messages } from '@salesforce/core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils'); -const IDENTITY_TOKEN_DESC = messages.getMessage('config-utils.token-desc'); +const IDENTITY_DATA_DESC = messages.getMessage('config-utils.data-desc'); const LOCAL_DEV_SERVER_CERT_DESC = messages.getMessage('config-utils.cert-desc'); const LOCAL_DEV_SERVER_CERT_ERROR_MESSAGE = messages.getMessage('config-utils.cert-error-message'); const LOCAL_DEV_SERVER_PORT_DESC = messages.getMessage('config-utils.port-desc'); @@ -27,8 +27,8 @@ export type SerializedSSLCertificateData = { export const enum ConfigVars { /** - * The identity data is a data structure that ties together a single - * local web server identity token to multiple orgs. + * The identity data is a data structure that links the local web server's + * identity token to the user's configured Salesforce orgs. */ LOCAL_WEB_SERVER_IDENTITY_DATA = 'local-web-server-identity-data', @@ -51,7 +51,7 @@ export const enum ConfigVars { export default [ { key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, - description: IDENTITY_TOKEN_DESC, + description: IDENTITY_DATA_DESC, hidden: true, encrypted: true, }, diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 25ae5f95..f22e5b17 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -5,10 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - import { Workspace } from '@lwc/lwc-dev-server'; import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; import { Config, ConfigAggregator, Connection } from '@salesforce/core'; @@ -17,14 +13,10 @@ import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../confi export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081; export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli; -export class LocalWebServerIdentityData { - public identityToken: string; - public usernameToServerEntityIdMap: Record = {}; - - public constructor(token: string) { - this.identityToken = token; - } -} +export type LocalWebServerIdentityData = { + identityToken: string; + usernameToServerEntityIdMap: Record; +}; export class ConfigUtils { static #config: Config; @@ -53,29 +45,30 @@ export class ConfigUtils { if (!identityData) { const token = CryptoUtils.generateIdentityToken(); const entityId = await this.saveIdentityTokenToServer(token, connection); - identityData = new LocalWebServerIdentityData(token); + identityData = { + identityToken: token, + usernameToServerEntityIdMap: {}, + }; identityData.usernameToServerEntityIdMap[username] = entityId; await this.writeIdentityData(identityData); return token; } else { - let entityId = identityData.usernameToServerEntityIdMap[username]; - if (!entityId) { - entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection); - identityData.usernameToServerEntityIdMap[username] = entityId; - await this.writeIdentityData(identityData); - } + const existingEntityId = identityData.usernameToServerEntityIdMap[username]; + const entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection, existingEntityId); + identityData.usernameToServerEntityIdMap[username] = entityId; + await this.writeIdentityData(identityData); return identityData.identityToken; } } public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise { - const config = await this.getGlobalConfig(); - config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, JSON.stringify(identityData)); + const config = await this.getConfig(); + config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, identityData); await config.write(); } public static async getCertData(): Promise { - const config = await this.getGlobalConfig(); + const config = await this.getConfig(); const serializedData = config.get(ConfigVars.LOCAL_DEV_SERVER_HTTPS_CERT_DATA) as SerializedSSLCertificateData; if (serializedData) { const deserializedData: SSLCertificateData = { @@ -121,17 +114,21 @@ export class ConfigUtils { const config = await ConfigAggregator.create({ customConfigMeta: configMeta }); // Need to reload to make sure the values read are decrypted await config.reload(); - const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA) as string; + const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA); if (identityJson) { - return JSON.parse(identityJson) as LocalWebServerIdentityData; + return identityJson as LocalWebServerIdentityData; } return undefined; } - private static async saveIdentityTokenToServer(token: string, connection: Connection): Promise { + private static async saveIdentityTokenToServer( + token: string, + connection: Connection, + entityId: string = '' + ): Promise { const sobject = connection.sobject('UserLocalWebServerIdentity'); - const result = await sobject.insert({ LocalWebServerIdentityToken: token }); + const result = await sobject.upsert({ LocalWebServerIdentityToken: token, Id: `${entityId}` }, 'ID'); if (result.success) { return result.id; } diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index 6b3cdbf5..9be42503 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -162,6 +162,7 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location. * * @param ldpServerUrl The URL for the local dev server + * @param token The identity token used for web socket handshake * @param appId An optional app id for a targeted LEX app * @param targetOrg An optional org id * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) @@ -169,6 +170,7 @@ export class PreviewUtils { */ public static generateDesktopPreviewLaunchArguments( ldpServerUrl: string, + token: string, appId?: string, targetOrg?: string, auraMode = DevPreviewAuraMode @@ -181,7 +183,10 @@ export class PreviewUtils { const appPath = appId ? `lightning/app/${appId}` : 'lightning'; // we prepend a '0.' to all of the params to ensure they will persist across browser redirects - const launchArguments = ['--path', `${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.mode=${auraMode}`]; + const launchArguments = [ + '--path', + `${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.ldpServerId=${token}&0.aura.mode=${auraMode}`, + ]; if (targetOrg) { launchArguments.push('--target-org', targetOrg); @@ -194,6 +199,7 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments. * * @param ldpServerUrl The URL for the local dev server + * @param token The identity token used for web socket handshake * @param appName An optional app name for a targeted LEX app * @param appId An optional app id for a targeted LEX app * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) @@ -201,6 +207,7 @@ export class PreviewUtils { */ public static generateMobileAppPreviewLaunchArguments( ldpServerUrl: string, + token: string, appName?: string, appId?: string, auraMode = DevPreviewAuraMode @@ -219,6 +226,8 @@ export class PreviewUtils { launchArguments.push({ name: '0.aura.mode', value: auraMode }); + launchArguments.push({ name: '0.aura.ldpServerId', value: token }); + return launchArguments; } diff --git a/test/shared/configUtils.test.ts b/test/shared/configUtils.test.ts index f5ebf87c..e7370236 100644 --- a/test/shared/configUtils.test.ts +++ b/test/shared/configUtils.test.ts @@ -5,8 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ - - import { expect } from 'chai'; import { Workspace } from '@lwc/lwc-dev-server'; import { Config, ConfigAggregator, Connection } from '@salesforce/core'; @@ -30,7 +28,10 @@ describe('configUtils', () => { }); it('getOrCreateIdentityToken resolves if identity data is found', async () => { - const identityData = new LocalWebServerIdentityData(fakeIdentityToken); + const identityData: LocalWebServerIdentityData = { + identityToken: fakeIdentityToken, + usernameToServerEntityIdMap: {}, + }; identityData.usernameToServerEntityIdMap[username] = 'entityId'; const stubConnection = $$.SANDBOX.createStubInstance(Connection); $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(identityData); @@ -83,7 +84,11 @@ describe('configUtils', () => { $$.SANDBOX.match.string ); $$.SANDBOX.stub(Config.prototype, 'write').resolves(); - const identityData = new LocalWebServerIdentityData(fakeIdentityToken); + const identityData: LocalWebServerIdentityData = { + identityToken: fakeIdentityToken, + usernameToServerEntityIdMap: {}, + }; + identityData.usernameToServerEntityIdMap[username] = 'entityId'; const resolved = await ConfigUtils.writeIdentityData(identityData); expect(resolved).to.equal(undefined); From 0c3a2d670511e2801dc00eedecf9b662a0293852 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 10 Jul 2024 16:15:24 -0700 Subject: [PATCH 4/9] chore: fix tests --- messages/lightning.preview.app.md | 8 +++++ src/commands/lightning/preview/app.ts | 16 ++++++--- src/shared/configUtils.ts | 11 ++---- src/shared/previewUtils.ts | 38 +++++++++++++++------ test/commands/lightning/preview/app.test.ts | 19 +++++++++-- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/messages/lightning.preview.app.md b/messages/lightning.preview.app.md index 364709f1..f11fb8bd 100644 --- a/messages/lightning.preview.app.md +++ b/messages/lightning.preview.app.md @@ -39,6 +39,14 @@ For mobile virtual devices, specify the device ID to preview. If omitted, the fi Org must have a valid user +# error.identitydata + +Couldn't find identity data while generating preview arguments + +# error.identitydata.entityid + +Couldn't find entity ID while generating preview arguments + # error.no-project This command is required to run from within a Salesforce project directory. %s diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index 5c2238fa..ec4ffb87 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -190,13 +190,14 @@ export default class LightningPreviewApp extends SfCommand { logger.debug(`Local Dev Server url is ${ldpServerUrl}`); if (platform === Platform.desktop) { - await this.desktopPreview(sfdxProjectRootPath, serverPort, token, ldpServerUrl, appId, logger); + await this.desktopPreview(sfdxProjectRootPath, serverPort, token, username, ldpServerUrl, appId, logger); } else { await this.mobilePreview( platform, sfdxProjectRootPath, serverPort, token, + username, ldpServerUrl, appName, appId, @@ -210,6 +211,7 @@ export default class LightningPreviewApp extends SfCommand { sfdxProjectRootPath: string, serverPort: number, token: string, + username: string, ldpServerUrl: string, appId: string | undefined, logger: Logger @@ -242,7 +244,12 @@ export default class LightningPreviewApp extends SfCommand { this.log(`\n${messages.getMessage('trust.local.dev.server')}`); } - const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, token, appId, targetOrg); + const launchArguments = await PreviewUtils.generateDesktopPreviewLaunchArguments( + ldpServerUrl, + username, + appId, + targetOrg + ); // Start the LWC Dev Server await startLWCServer(logger, sfdxProjectRootPath, token, serverPort); @@ -256,6 +263,7 @@ export default class LightningPreviewApp extends SfCommand { sfdxProjectRootPath: string, serverPort: number, token: string, + username: string, ldpServerUrl: string, appName: string | undefined, appId: string | undefined, @@ -334,9 +342,9 @@ export default class LightningPreviewApp extends SfCommand { // Launch the native app for previewing (launchMobileApp will show its own spinner) // eslint-disable-next-line camelcase - appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments( + appConfig.launch_arguments = await PreviewUtils.generateMobileAppPreviewLaunchArguments( ldpServerUrl, - token, + username, appName, appId ); diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index f22e5b17..792ee89f 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -53,8 +53,7 @@ export class ConfigUtils { await this.writeIdentityData(identityData); return token; } else { - const existingEntityId = identityData.usernameToServerEntityIdMap[username]; - const entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection, existingEntityId); + const entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection); identityData.usernameToServerEntityIdMap[username] = entityId; await this.writeIdentityData(identityData); return identityData.identityToken; @@ -122,13 +121,9 @@ export class ConfigUtils { return undefined; } - private static async saveIdentityTokenToServer( - token: string, - connection: Connection, - entityId: string = '' - ): Promise { + private static async saveIdentityTokenToServer(token: string, connection: Connection): Promise { const sobject = connection.sobject('UserLocalWebServerIdentity'); - const result = await sobject.upsert({ LocalWebServerIdentityToken: token, Id: `${entityId}` }, 'ID'); + const result = await sobject.insert({ LocalWebServerIdentityToken: token }); if (result.success) { return result.id; } diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index 9be42503..89469250 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -162,19 +162,19 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location. * * @param ldpServerUrl The URL for the local dev server - * @param token The identity token used for web socket handshake + * @param username User that is initiating the local preview * @param appId An optional app id for a targeted LEX app * @param targetOrg An optional org id * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) * @returns Array of arguments to be used by Org:Open command for launching desktop browser */ - public static generateDesktopPreviewLaunchArguments( + public static async generateDesktopPreviewLaunchArguments( ldpServerUrl: string, - token: string, + username: string, appId?: string, targetOrg?: string, auraMode = DevPreviewAuraMode - ): string[] { + ): Promise { // appPath will resolve to one of the following: // // lightning/app/ => when the user is targeting a specific LEX app @@ -182,10 +182,12 @@ export class PreviewUtils { // const appPath = appId ? `lightning/app/${appId}` : 'lightning'; + const entityId = await this.getEntityId(username); + // we prepend a '0.' to all of the params to ensure they will persist across browser redirects const launchArguments = [ '--path', - `${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.ldpServerId=${token}&0.aura.mode=${auraMode}`, + `${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.ldpServerId=${entityId}&0.aura.mode=${auraMode}`, ]; if (targetOrg) { @@ -199,19 +201,19 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments. * * @param ldpServerUrl The URL for the local dev server - * @param token The identity token used for web socket handshake + * @param username User that is initiating the local preview * @param appName An optional app name for a targeted LEX app * @param appId An optional app id for a targeted LEX app * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) * @returns Array of arguments to be used as custom launch arguments when launching a mobile app. */ - public static generateMobileAppPreviewLaunchArguments( + public static async generateMobileAppPreviewLaunchArguments( ldpServerUrl: string, - token: string, + username: string, appName?: string, appId?: string, auraMode = DevPreviewAuraMode - ): LaunchArgument[] { + ): Promise { const launchArguments: LaunchArgument[] = []; if (appName) { @@ -226,7 +228,9 @@ export class PreviewUtils { launchArguments.push({ name: '0.aura.mode', value: auraMode }); - launchArguments.push({ name: '0.aura.ldpServerId', value: token }); + const entityId = await this.getEntityId(username); + + launchArguments.push({ name: '0.aura.ldpServerId', value: entityId }); return launchArguments; } @@ -504,4 +508,18 @@ export class PreviewUtils { logger?.debug(`Extracting archive ${zipFilePath}`); await CommonUtils.executeCommandAsync(cmd, logger); } + + private static async getEntityId(username: string): Promise { + const identityData = await ConfigUtils.getIdentityData(); + let entityId: string | undefined; + if (!identityData) { + throw new Error(messages.getMessage('error.identitydata')); + } else { + entityId = identityData.usernameToServerEntityIdMap[username]; + if (!entityId) { + throw new Error(messages.getMessage('error.identitydata.entityid')); + } + return entityId; + } + } } diff --git a/test/commands/lightning/preview/app.test.ts b/test/commands/lightning/preview/app.test.ts index 4585c5fb..478df1e6 100644 --- a/test/commands/lightning/preview/app.test.ts +++ b/test/commands/lightning/preview/app.test.ts @@ -25,6 +25,7 @@ import LightningPreviewApp, { } from '../../../../src/commands/lightning/preview/app.js'; import { OrgUtils } from '../../../../src/shared/orgUtils.js'; import { PreviewUtils } from '../../../../src/shared/previewUtils.js'; +import { ConfigUtils, LocalWebServerIdentityData } from '../../../../src/shared/configUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); @@ -52,6 +53,14 @@ describe('lightning preview app', () => { const testEmulatorPort = 1234; let MockedLightningPreviewApp: typeof LightningPreviewApp; + const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; + const fakeEntityId = '1I9xx0000004ClkCAE'; + const fakeIdentityData: LocalWebServerIdentityData = { + identityToken: `${fakeIdentityToken}`, + usernameToServerEntityIdMap: {}, + }; + fakeIdentityData.usernameToServerEntityIdMap[testOrgData.username] = fakeEntityId; + beforeEach(async () => { stubUx($$.SANDBOX); stubSpinner($$.SANDBOX); @@ -62,6 +71,7 @@ describe('lightning preview app', () => { $$.SANDBOX.stub(SfConfig.prototype, 'get').returns(undefined); $$.SANDBOX.stub(SfConfig.prototype, 'set'); $$.SANDBOX.stub(SfConfig.prototype, 'write').resolves(); + $$.SANDBOX.stub(ConfigUtils, 'getOrCreateIdentityToken').resolves(fakeIdentityToken); MockedLightningPreviewApp = await esmock( '../../../../src/commands/lightning/preview/app.js', @@ -112,6 +122,8 @@ describe('lightning preview app', () => { async function verifyOrgOpen(expectedAppPath: string, appName?: string): Promise { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(fakeIdentityData); + const runCmdStub = $$.SANDBOX.stub(OclifConfig.prototype, 'runCommand').resolves(); if (appName) { await MockedLightningPreviewApp.run(['--name', appName, '-o', testOrgData.username]); @@ -124,7 +136,7 @@ describe('lightning preview app', () => { 'org:open', [ '--path', - `${expectedAppPath}?0.aura.ldpServerUrl=${testServerUrl}&0.aura.mode=DEVPREVIEW`, + `${expectedAppPath}?0.aura.ldpServerUrl=${testServerUrl}&0.aura.ldpServerId=${fakeEntityId}&0.aura.mode=DEVPREVIEW`, '--target-org', testOrgData.username, ], @@ -196,6 +208,7 @@ describe('lightning preview app', () => { it('waits for user to manually install the certificate', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(fakeIdentityData); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -272,6 +285,7 @@ describe('lightning preview app', () => { it('installs and launched app on mobile device', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(fakeIdentityData); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -413,8 +427,9 @@ describe('lightning preview app', () => { const expectedAppConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig; // eslint-disable-next-line camelcase - expectedAppConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments( + expectedAppConfig.launch_arguments = await PreviewUtils.generateMobileAppPreviewLaunchArguments( expectedLdpServerUrl, + testOrgData.username, 'Sales', testAppId ); From c8d2672813fddb12114748c3de29fea91d6fdb3a Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Wed, 10 Jul 2024 17:38:28 -0700 Subject: [PATCH 5/9] chore: touch up after rebase --- test/lwc-dev-server/index.e2e.test.ts | 2 +- test/shared/configUtils.test.ts | 7 +------ test/shared/previewUtils.test.ts | 30 ++++++++++++++++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/test/lwc-dev-server/index.e2e.test.ts b/test/lwc-dev-server/index.e2e.test.ts index 4c67b32e..7bf9e870 100644 --- a/test/lwc-dev-server/index.e2e.test.ts +++ b/test/lwc-dev-server/index.e2e.test.ts @@ -46,7 +46,7 @@ // $$.SANDBOX.resetHistory(); // }); - it('e2e', async () => { +// it('e2e', async () => { // const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; // const server = await devServer.startLWCServer(logger, path.resolve(__dirname, './__mocks__'), fakeIdentityToken); diff --git a/test/shared/configUtils.test.ts b/test/shared/configUtils.test.ts index e7370236..a3ddcdae 100644 --- a/test/shared/configUtils.test.ts +++ b/test/shared/configUtils.test.ts @@ -10,12 +10,7 @@ import { Workspace } from '@lwc/lwc-dev-server'; import { Config, ConfigAggregator, Connection } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { CryptoUtils } from '@salesforce/lwc-dev-mobile-core'; -import { - ConfigUtils, - LOCAL_DEV_SERVER_DEFAULT_PORT, - LOCAL_DEV_SERVER_DEFAULT_WORKSPACE, - LocalWebServerIdentityData, -} from '../../src/shared/configUtils.js'; +import { ConfigUtils, LocalWebServerIdentityData } from '../../src/shared/configUtils.js'; import { ConfigVars } from '../../src/configMeta.js'; describe('configUtils', () => { diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index 01a30c62..7c5c9eb9 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -44,6 +44,9 @@ describe('previewUtils', () => { '34' ); + const username = 'SalesforceDeveloper'; + const entityId = '1I9xx0000004ClkCAE'; + afterEach(() => { $$.restore(); }); @@ -107,22 +110,38 @@ describe('previewUtils', () => { }); it('generateDesktopPreviewLaunchArguments', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') + .withArgs([username]) + .resolves(entityId); + expect( - PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', 'MyAppId', 'MyTargetOrg', 'MyAuraMode') + PreviewUtils.generateDesktopPreviewLaunchArguments( + 'MyLdpServerUrl', + username, + 'MyAppId', + 'MyTargetOrg', + 'MyAuraMode' + ) ).to.deep.equal([ '--path', - 'lightning/app/MyAppId?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.mode=MyAuraMode', + `lightning/app/MyAppId?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${entityId}&0.aura.mode=MyAuraMode`, '--target-org', 'MyTargetOrg', ]); - expect(PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl')).to.deep.equal([ + expect(PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ '--path', - 'lightning?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.mode=DEVPREVIEW', + `lightning?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${entityId}&0.aura.mode=DEVPREVIEW`, ]); }); it('generateMobileAppPreviewLaunchArguments', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') + .withArgs([username]) + .resolves(entityId); + expect( PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', 'MyAppName', 'MyAppId', 'MyAuraMode') ).to.deep.equal([ @@ -132,9 +151,10 @@ describe('previewUtils', () => { { name: '0.aura.mode', value: 'MyAuraMode' }, ]); - expect(PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl')).to.deep.equal([ + expect(PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ { name: '0.aura.ldpServerUrl', value: 'MyLdpServerUrl' }, { name: '0.aura.mode', value: 'DEVPREVIEW' }, + { name: '0.aura.ldpServerId', value: entityId }, ]); }); From dca0d31427e4bee5cdb70743cad894869333c047 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Thu, 11 Jul 2024 11:22:44 -0700 Subject: [PATCH 6/9] chore: refactor usage of connection object. fix more tests --- src/commands/lightning/preview/app.ts | 5 ++- src/shared/configUtils.ts | 42 ++++++++++++++------- test/commands/lightning/preview/app.test.ts | 12 +++++- test/shared/configUtils.test.ts | 33 ++++++++++------ test/shared/previewUtils.test.ts | 19 +++++++--- 5 files changed, 76 insertions(+), 35 deletions(-) diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index ec4ffb87..12d41604 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -21,7 +21,7 @@ import chalk from 'chalk'; import { OrgUtils } from '../../../shared/orgUtils.js'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; -import { ConfigUtils } from '../../../shared/configUtils.js'; +import { AppServerIdentityTokenService, ConfigUtils } from '../../../shared/configUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app'); @@ -165,7 +165,8 @@ export default class LightningPreviewApp extends SfCommand { return Promise.reject(new Error(messages.getMessage('error.username'))); } - const token = await ConfigUtils.getOrCreateIdentityToken(username, connection); + const tokenService = new AppServerIdentityTokenService(connection); + const token = await ConfigUtils.getOrCreateIdentityToken(username, tokenService); let appId: string | undefined; if (appName) { diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 792ee89f..6a99c15e 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -10,6 +10,27 @@ import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core import { Config, ConfigAggregator, Connection } from '@salesforce/core'; import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../configMeta.js'; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export interface IdentityTokenService { + saveTokenToServer(token: string): Promise; +} + +export class AppServerIdentityTokenService implements IdentityTokenService { + private connection: Connection; + public constructor(connection: Connection) { + this.connection = connection; + } + + public async saveTokenToServer(token: string): Promise { + const sobject = this.connection.sobject('UserLocalWebServerIdentity'); + const result = await sobject.insert({ LocalWebServerIdentityToken: token }); + if (result.success) { + return result.id; + } + throw new Error('Could not save the token to the server'); + } +} + export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081; export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli; @@ -40,11 +61,11 @@ export class ConfigUtils { return this.#globalConfig; } - public static async getOrCreateIdentityToken(username: string, connection: Connection): Promise { + public static async getOrCreateIdentityToken(username: string, tokenService: IdentityTokenService): Promise { let identityData = await this.getIdentityData(); if (!identityData) { const token = CryptoUtils.generateIdentityToken(); - const entityId = await this.saveIdentityTokenToServer(token, connection); + const entityId = await tokenService.saveTokenToServer(token); identityData = { identityToken: token, usernameToServerEntityIdMap: {}, @@ -53,7 +74,7 @@ export class ConfigUtils { await this.writeIdentityData(identityData); return token; } else { - const entityId = await this.saveIdentityTokenToServer(identityData.identityToken, connection); + const entityId = await tokenService.saveTokenToServer(identityData.identityToken); identityData.usernameToServerEntityIdMap[username] = entityId; await this.writeIdentityData(identityData); return identityData.identityToken; @@ -62,7 +83,9 @@ export class ConfigUtils { public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise { const config = await this.getConfig(); - config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, identityData); + // TODO: JSON needs to be stringified in order for config.write to encrypt. When config.write() + // can encrypt JSON data to write it into config we shall remove stringify(). + config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, JSON.stringify(identityData)); await config.write(); } @@ -116,17 +139,8 @@ export class ConfigUtils { const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA); if (identityJson) { - return identityJson as LocalWebServerIdentityData; + return JSON.parse(identityJson as string) as LocalWebServerIdentityData; } return undefined; } - - private static async saveIdentityTokenToServer(token: string, connection: Connection): Promise { - const sobject = connection.sobject('UserLocalWebServerIdentity'); - const result = await sobject.insert({ LocalWebServerIdentityToken: token }); - if (result.success) { - return result.id; - } - throw new Error('Could not save the token to the server'); - } } diff --git a/test/commands/lightning/preview/app.test.ts b/test/commands/lightning/preview/app.test.ts index 478df1e6..fe123f44 100644 --- a/test/commands/lightning/preview/app.test.ts +++ b/test/commands/lightning/preview/app.test.ts @@ -7,7 +7,7 @@ import path from 'node:path'; import { Config as OclifConfig } from '@oclif/core'; -import { Config as SfConfig, Messages } from '@salesforce/core'; +import { Config as SfConfig, Messages, Connection } from '@salesforce/core'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import { AndroidVirtualDevice, @@ -98,6 +98,16 @@ describe('lightning preview app', () => { } }); + it('throws when username not found', async () => { + try { + $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(undefined); + $$.SANDBOX.stub(Connection.prototype, 'getUsername').returns(undefined); + await MockedLightningPreviewApp.run(['--name', 'blah', '-o', testOrgData.username]); + } catch (err) { + expect(err).to.be.an('error').with.property('message', messages.getMessage('error.username')); + } + }); + it('throws when cannot determine ldp server url', async () => { try { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); diff --git a/test/shared/configUtils.test.ts b/test/shared/configUtils.test.ts index a3ddcdae..4a4ac14f 100644 --- a/test/shared/configUtils.test.ts +++ b/test/shared/configUtils.test.ts @@ -10,13 +10,22 @@ import { Workspace } from '@lwc/lwc-dev-server'; import { Config, ConfigAggregator, Connection } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { CryptoUtils } from '@salesforce/lwc-dev-mobile-core'; -import { ConfigUtils, LocalWebServerIdentityData } from '../../src/shared/configUtils.js'; +import { ConfigUtils, LocalWebServerIdentityData, IdentityTokenService } from '../../src/shared/configUtils.js'; import { ConfigVars } from '../../src/configMeta.js'; describe('configUtils', () => { const $$ = new TestContext(); const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; const username = 'SalesforceDeveloper'; + const fakeEntityId = 'entityId'; + + class TestIdentityTokenService implements IdentityTokenService { + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + public saveTokenToServer(token: string): Promise { + return Promise.resolve(fakeEntityId); + } + } + const testTokenService = new TestIdentityTokenService(); afterEach(() => { $$.restore(); @@ -27,27 +36,22 @@ describe('configUtils', () => { identityToken: fakeIdentityToken, usernameToServerEntityIdMap: {}, }; - identityData.usernameToServerEntityIdMap[username] = 'entityId'; - const stubConnection = $$.SANDBOX.createStubInstance(Connection); + identityData.usernameToServerEntityIdMap[username] = fakeEntityId; $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(identityData); $$.SANDBOX.stub(Connection, 'create').resolves(Connection.prototype); + $$.SANDBOX.stub(ConfigUtils, 'writeIdentityData').resolves(); - const resolved = await ConfigUtils.getOrCreateIdentityToken(username, stubConnection); + const resolved = await ConfigUtils.getOrCreateIdentityToken(username, testTokenService); expect(resolved).to.equal(fakeIdentityToken); }); it('getOrCreateIdentityToken resolves and writeIdentityData is called when there is no identity data', async () => { - const stubConnection = $$.SANDBOX.createStubInstance(Connection); $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(undefined); $$.SANDBOX.stub(CryptoUtils, 'generateIdentityToken').resolves(fakeIdentityToken); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - $$.SANDBOX.stub(ConfigUtils as any, 'saveIdentityTokenToServer') - .withArgs(fakeIdentityToken, stubConnection) - .resolves('entityId'); const writeIdentityTokenStub = $$.SANDBOX.stub(ConfigUtils, 'writeIdentityData').resolves(); - const resolved = await ConfigUtils.getOrCreateIdentityToken(username, stubConnection); + const resolved = await ConfigUtils.getOrCreateIdentityToken(username, testTokenService); expect(resolved).to.equal(fakeIdentityToken); expect(writeIdentityTokenStub.calledOnce).to.be.true; @@ -63,12 +67,17 @@ describe('configUtils', () => { }); it('getIdentityData resolves when identity data is available', async () => { + const identityData: LocalWebServerIdentityData = { + identityToken: fakeIdentityToken, + usernameToServerEntityIdMap: {}, + }; + const stringifiedData = JSON.stringify(identityData); $$.SANDBOX.stub(ConfigAggregator, 'create').resolves(ConfigAggregator.prototype); $$.SANDBOX.stub(ConfigAggregator.prototype, 'reload').resolves(); - $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(fakeIdentityToken); + $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(stringifiedData); const resolved = await ConfigUtils.getIdentityData(); - expect(resolved).to.equal(fakeIdentityToken); + expect(resolved).to.deep.equal(identityData); }); it('writeIdentityData resolves', async () => { diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index 7c5c9eb9..737c32d3 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -112,11 +112,11 @@ describe('previewUtils', () => { it('generateDesktopPreviewLaunchArguments', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') - .withArgs([username]) + .withArgs(username) .resolves(entityId); expect( - PreviewUtils.generateDesktopPreviewLaunchArguments( + await PreviewUtils.generateDesktopPreviewLaunchArguments( 'MyLdpServerUrl', username, 'MyAppId', @@ -130,7 +130,7 @@ describe('previewUtils', () => { 'MyTargetOrg', ]); - expect(PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ + expect(await PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ '--path', `lightning?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${entityId}&0.aura.mode=DEVPREVIEW`, ]); @@ -139,19 +139,26 @@ describe('previewUtils', () => { it('generateMobileAppPreviewLaunchArguments', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') - .withArgs([username]) + .withArgs(username) .resolves(entityId); expect( - PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', 'MyAppName', 'MyAppId', 'MyAuraMode') + await PreviewUtils.generateMobileAppPreviewLaunchArguments( + 'MyLdpServerUrl', + username, + 'MyAppName', + 'MyAppId', + 'MyAuraMode' + ) ).to.deep.equal([ { name: 'LightningExperienceAppName', value: 'MyAppName' }, { name: 'LightningExperienceAppID', value: 'MyAppId' }, { name: '0.aura.ldpServerUrl', value: 'MyLdpServerUrl' }, { name: '0.aura.mode', value: 'MyAuraMode' }, + { name: '0.aura.ldpServerId', value: entityId }, ]); - expect(PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ + expect(await PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ { name: '0.aura.ldpServerUrl', value: 'MyLdpServerUrl' }, { name: '0.aura.mode', value: 'DEVPREVIEW' }, { name: '0.aura.ldpServerId', value: entityId }, From f6aead9fe12af170fbc22de43560e4025c651609 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Thu, 11 Jul 2024 13:42:03 -0700 Subject: [PATCH 7/9] chore: remove interface --- src/commands/lightning/preview/app.ts | 20 ++++++++++++++++++-- src/shared/configUtils.ts | 23 +++-------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index 12d41604..d71e8a2a 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -7,7 +7,7 @@ import path from 'node:path'; import * as readline from 'node:readline'; -import { Logger, Messages, SfProject } from '@salesforce/core'; +import { Connection, Logger, Messages, SfProject } from '@salesforce/core'; import { AndroidAppPreviewConfig, AndroidVirtualDevice, @@ -21,7 +21,7 @@ import chalk from 'chalk'; import { OrgUtils } from '../../../shared/orgUtils.js'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; -import { AppServerIdentityTokenService, ConfigUtils } from '../../../shared/configUtils.js'; +import { ConfigUtils, IdentityTokenService } from '../../../shared/configUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app'); @@ -39,6 +39,22 @@ export const androidSalesforceAppPreviewConfig = { const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value +class AppServerIdentityTokenService implements IdentityTokenService { + private connection: Connection; + public constructor(connection: Connection) { + this.connection = connection; + } + + public async saveTokenToServer(token: string): Promise { + const sobject = this.connection.sobject('UserLocalWebServerIdentity'); + const result = await sobject.insert({ LocalWebServerIdentityToken: token }); + if (result.success) { + return result.id; + } + throw new Error('Could not save the token to the server'); + } +} + export default class LightningPreviewApp extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 6a99c15e..2218da62 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -7,29 +7,12 @@ import { Workspace } from '@lwc/lwc-dev-server'; import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; -import { Config, ConfigAggregator, Connection } from '@salesforce/core'; +import { Config, ConfigAggregator } from '@salesforce/core'; import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../configMeta.js'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface IdentityTokenService { +export type IdentityTokenService = { saveTokenToServer(token: string): Promise; -} - -export class AppServerIdentityTokenService implements IdentityTokenService { - private connection: Connection; - public constructor(connection: Connection) { - this.connection = connection; - } - - public async saveTokenToServer(token: string): Promise { - const sobject = this.connection.sobject('UserLocalWebServerIdentity'); - const result = await sobject.insert({ LocalWebServerIdentityToken: token }); - if (result.success) { - return result.id; - } - throw new Error('Could not save the token to the server'); - } -} +}; export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081; export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli; From 28f44c33c88807cd8e3199840e9d6e93a35e46f2 Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Thu, 11 Jul 2024 13:50:53 -0700 Subject: [PATCH 8/9] chore: nit --- src/shared/configUtils.ts | 20 ++++++++++---------- src/shared/previewUtils.ts | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 2218da62..96e95f16 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -23,16 +23,16 @@ export type LocalWebServerIdentityData = { }; export class ConfigUtils { - static #config: Config; + static #localConfig: Config; static #globalConfig: Config; - public static async getConfig(): Promise { - if (this.#config) { - return this.#config; + public static async getLocalConfig(): Promise { + if (this.#localConfig) { + return this.#localConfig; } - this.#config = await Config.create({ isGlobal: false }); + this.#localConfig = await Config.create({ isGlobal: false }); Config.addAllowedProperties(configMeta); - return this.#config; + return this.#localConfig; } public static async getGlobalConfig(): Promise { @@ -65,7 +65,7 @@ export class ConfigUtils { } public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise { - const config = await this.getConfig(); + const config = await this.getLocalConfig(); // TODO: JSON needs to be stringified in order for config.write to encrypt. When config.write() // can encrypt JSON data to write it into config we shall remove stringify(). config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, JSON.stringify(identityData)); @@ -73,7 +73,7 @@ export class ConfigUtils { } public static async getCertData(): Promise { - const config = await this.getConfig(); + const config = await this.getLocalConfig(); const serializedData = config.get(ConfigVars.LOCAL_DEV_SERVER_HTTPS_CERT_DATA) as SerializedSSLCertificateData; if (serializedData) { const deserializedData: SSLCertificateData = { @@ -102,14 +102,14 @@ export class ConfigUtils { } public static async getLocalDevServerPort(): Promise { - const config = await this.getConfig(); + const config = await this.getLocalConfig(); const configPort = config.get(ConfigVars.LOCAL_DEV_SERVER_PORT) as number; return configPort; } public static async getLocalDevServerWorkspace(): Promise { - const config = await this.getConfig(); + const config = await this.getLocalConfig(); const configWorkspace = config.get(ConfigVars.LOCAL_DEV_SERVER_WORKSPACE) as Workspace; return configWorkspace; diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index 89469250..a493512d 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -513,11 +513,11 @@ export class PreviewUtils { const identityData = await ConfigUtils.getIdentityData(); let entityId: string | undefined; if (!identityData) { - throw new Error(messages.getMessage('error.identitydata')); + return Promise.reject(new Error(messages.getMessage('error.identitydata'))); } else { entityId = identityData.usernameToServerEntityIdMap[username]; if (!entityId) { - throw new Error(messages.getMessage('error.identitydata.entityid')); + return Promise.reject(messages.getMessage('error.identitydata.entityid')); } return entityId; } From 2b572429abe0626a621a4556ba19c087245f23eb Mon Sep 17 00:00:00 2001 From: Takashi Arai Date: Fri, 12 Jul 2024 14:57:52 -0700 Subject: [PATCH 9/9] chore: nit --- src/commands/lightning/preview/app.ts | 18 +++--- src/shared/configUtils.ts | 9 ++- src/shared/previewUtils.ts | 24 +++---- test/commands/lightning/preview/app.test.ts | 10 ++- test/shared/previewUtils.test.ts | 72 +++++++++++++++++---- 5 files changed, 92 insertions(+), 41 deletions(-) diff --git a/src/commands/lightning/preview/app.ts b/src/commands/lightning/preview/app.ts index d71e8a2a..7f1a3f9a 100644 --- a/src/commands/lightning/preview/app.ts +++ b/src/commands/lightning/preview/app.ts @@ -206,15 +206,17 @@ export default class LightningPreviewApp extends SfCommand { const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPort, logger); logger.debug(`Local Dev Server url is ${ldpServerUrl}`); + const entityId = await PreviewUtils.getEntityId(username); + if (platform === Platform.desktop) { - await this.desktopPreview(sfdxProjectRootPath, serverPort, token, username, ldpServerUrl, appId, logger); + await this.desktopPreview(sfdxProjectRootPath, serverPort, token, entityId, ldpServerUrl, appId, logger); } else { await this.mobilePreview( platform, sfdxProjectRootPath, serverPort, token, - username, + entityId, ldpServerUrl, appName, appId, @@ -228,7 +230,7 @@ export default class LightningPreviewApp extends SfCommand { sfdxProjectRootPath: string, serverPort: number, token: string, - username: string, + entityId: string, ldpServerUrl: string, appId: string | undefined, logger: Logger @@ -261,9 +263,9 @@ export default class LightningPreviewApp extends SfCommand { this.log(`\n${messages.getMessage('trust.local.dev.server')}`); } - const launchArguments = await PreviewUtils.generateDesktopPreviewLaunchArguments( + const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments( ldpServerUrl, - username, + entityId, appId, targetOrg ); @@ -280,7 +282,7 @@ export default class LightningPreviewApp extends SfCommand { sfdxProjectRootPath: string, serverPort: number, token: string, - username: string, + entityId: string, ldpServerUrl: string, appName: string | undefined, appId: string | undefined, @@ -359,9 +361,9 @@ export default class LightningPreviewApp extends SfCommand { // Launch the native app for previewing (launchMobileApp will show its own spinner) // eslint-disable-next-line camelcase - appConfig.launch_arguments = await PreviewUtils.generateMobileAppPreviewLaunchArguments( + appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments( ldpServerUrl, - username, + entityId, appName, appId ); diff --git a/src/shared/configUtils.ts b/src/shared/configUtils.ts index 96e95f16..6e2cbfb2 100644 --- a/src/shared/configUtils.ts +++ b/src/shared/configUtils.ts @@ -57,9 +57,12 @@ export class ConfigUtils { await this.writeIdentityData(identityData); return token; } else { - const entityId = await tokenService.saveTokenToServer(identityData.identityToken); - identityData.usernameToServerEntityIdMap[username] = entityId; - await this.writeIdentityData(identityData); + let entityId = identityData.usernameToServerEntityIdMap[username]; + if (!entityId) { + entityId = await tokenService.saveTokenToServer(identityData.identityToken); + identityData.usernameToServerEntityIdMap[username] = entityId; + await this.writeIdentityData(identityData); + } return identityData.identityToken; } } diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index a493512d..908f6f8e 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -162,19 +162,19 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location. * * @param ldpServerUrl The URL for the local dev server - * @param username User that is initiating the local preview + * @param entityId Record ID for the identity token * @param appId An optional app id for a targeted LEX app * @param targetOrg An optional org id * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) * @returns Array of arguments to be used by Org:Open command for launching desktop browser */ - public static async generateDesktopPreviewLaunchArguments( + public static generateDesktopPreviewLaunchArguments( ldpServerUrl: string, - username: string, + entityId: string, appId?: string, targetOrg?: string, auraMode = DevPreviewAuraMode - ): Promise { + ): string[] { // appPath will resolve to one of the following: // // lightning/app/ => when the user is targeting a specific LEX app @@ -182,8 +182,6 @@ export class PreviewUtils { // const appPath = appId ? `lightning/app/${appId}` : 'lightning'; - const entityId = await this.getEntityId(username); - // we prepend a '0.' to all of the params to ensure they will persist across browser redirects const launchArguments = [ '--path', @@ -201,19 +199,19 @@ export class PreviewUtils { * Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments. * * @param ldpServerUrl The URL for the local dev server - * @param username User that is initiating the local preview + * @param entityId Record ID for the identity token * @param appName An optional app name for a targeted LEX app * @param appId An optional app id for a targeted LEX app * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) * @returns Array of arguments to be used as custom launch arguments when launching a mobile app. */ - public static async generateMobileAppPreviewLaunchArguments( + public static generateMobileAppPreviewLaunchArguments( ldpServerUrl: string, - username: string, + entityId: string, appName?: string, appId?: string, auraMode = DevPreviewAuraMode - ): Promise { + ): LaunchArgument[] { const launchArguments: LaunchArgument[] = []; if (appName) { @@ -228,8 +226,6 @@ export class PreviewUtils { launchArguments.push({ name: '0.aura.mode', value: auraMode }); - const entityId = await this.getEntityId(username); - launchArguments.push({ name: '0.aura.ldpServerId', value: entityId }); return launchArguments; @@ -509,7 +505,7 @@ export class PreviewUtils { await CommonUtils.executeCommandAsync(cmd, logger); } - private static async getEntityId(username: string): Promise { + public static async getEntityId(username: string): Promise { const identityData = await ConfigUtils.getIdentityData(); let entityId: string | undefined; if (!identityData) { @@ -517,7 +513,7 @@ export class PreviewUtils { } else { entityId = identityData.usernameToServerEntityIdMap[username]; if (!entityId) { - return Promise.reject(messages.getMessage('error.identitydata.entityid')); + return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid'))); } return entityId; } diff --git a/test/commands/lightning/preview/app.test.ts b/test/commands/lightning/preview/app.test.ts index fe123f44..b597f880 100644 --- a/test/commands/lightning/preview/app.test.ts +++ b/test/commands/lightning/preview/app.test.ts @@ -158,6 +158,7 @@ describe('lightning preview app', () => { it('throws when environment setup requirements are not met', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').rejects(new Error('Requirement blah not met')); @@ -169,6 +170,7 @@ describe('lightning preview app', () => { it('throws when unable to fetch mobile device', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -182,6 +184,7 @@ describe('lightning preview app', () => { it('throws when device fails to boot', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -199,6 +202,7 @@ describe('lightning preview app', () => { it('throws when cannot generate certificate', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -263,6 +267,7 @@ describe('lightning preview app', () => { it('throws if user chooses not to install app on mobile device', async () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -296,6 +301,7 @@ describe('lightning preview app', () => { $$.SANDBOX.stub(OrgUtils, 'getAppId').resolves(testAppId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').returns(testServerUrl); $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(fakeIdentityData); + $$.SANDBOX.stub(PreviewUtils, 'getEntityId').resolves(fakeEntityId); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'init').resolves(); $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); @@ -437,9 +443,9 @@ describe('lightning preview app', () => { const expectedAppConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig; // eslint-disable-next-line camelcase - expectedAppConfig.launch_arguments = await PreviewUtils.generateMobileAppPreviewLaunchArguments( + expectedAppConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments( expectedLdpServerUrl, - testOrgData.username, + fakeEntityId, 'Sales', testAppId ); diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index 737c32d3..3c51a073 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -18,7 +18,12 @@ import { Platform, SSLCertificateData, } from '@salesforce/lwc-dev-mobile-core'; -import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_PORT } from '../../src/shared/configUtils.js'; +import { Messages } from '@salesforce/core'; +import { + ConfigUtils, + LOCAL_DEV_SERVER_DEFAULT_PORT, + LocalWebServerIdentityData, +} from '../../src/shared/configUtils.js'; import { PreviewUtils } from '../../src/shared/previewUtils.js'; import { iOSSalesforceAppPreviewConfig, @@ -26,6 +31,7 @@ import { } from '../../src/commands/lightning/preview/app.js'; describe('previewUtils', () => { + const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app'); const $$ = new TestContext(); const testIOSDevice = new IOSSimulatorDevice( @@ -45,7 +51,8 @@ describe('previewUtils', () => { ); const username = 'SalesforceDeveloper'; - const entityId = '1I9xx0000004ClkCAE'; + const fakeEntityId = '1I9xx0000004ClkCAE'; + const fakeIdentityToken = 'PFT1vw8v65aXd2b9HFvZ3Zu4OcKZwjI60bq7BEjj5k4='; afterEach(() => { $$.restore(); @@ -113,26 +120,26 @@ describe('previewUtils', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') .withArgs(username) - .resolves(entityId); + .resolves(fakeEntityId); expect( - await PreviewUtils.generateDesktopPreviewLaunchArguments( + PreviewUtils.generateDesktopPreviewLaunchArguments( 'MyLdpServerUrl', - username, + fakeEntityId, 'MyAppId', 'MyTargetOrg', 'MyAuraMode' ) ).to.deep.equal([ '--path', - `lightning/app/MyAppId?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${entityId}&0.aura.mode=MyAuraMode`, + `lightning/app/MyAppId?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${fakeEntityId}&0.aura.mode=MyAuraMode`, '--target-org', 'MyTargetOrg', ]); - expect(await PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ + expect(PreviewUtils.generateDesktopPreviewLaunchArguments('MyLdpServerUrl', fakeEntityId)).to.deep.equal([ '--path', - `lightning?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${entityId}&0.aura.mode=DEVPREVIEW`, + `lightning?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${fakeEntityId}&0.aura.mode=DEVPREVIEW`, ]); }); @@ -140,12 +147,12 @@ describe('previewUtils', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any $$.SANDBOX.stub(PreviewUtils as any, 'getEntityId') .withArgs(username) - .resolves(entityId); + .resolves(fakeEntityId); expect( - await PreviewUtils.generateMobileAppPreviewLaunchArguments( + PreviewUtils.generateMobileAppPreviewLaunchArguments( 'MyLdpServerUrl', - username, + fakeEntityId, 'MyAppName', 'MyAppId', 'MyAuraMode' @@ -155,13 +162,13 @@ describe('previewUtils', () => { { name: 'LightningExperienceAppID', value: 'MyAppId' }, { name: '0.aura.ldpServerUrl', value: 'MyLdpServerUrl' }, { name: '0.aura.mode', value: 'MyAuraMode' }, - { name: '0.aura.ldpServerId', value: entityId }, + { name: '0.aura.ldpServerId', value: fakeEntityId }, ]); - expect(await PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', username)).to.deep.equal([ + expect(PreviewUtils.generateMobileAppPreviewLaunchArguments('MyLdpServerUrl', fakeEntityId)).to.deep.equal([ { name: '0.aura.ldpServerUrl', value: 'MyLdpServerUrl' }, { name: '0.aura.mode', value: 'DEVPREVIEW' }, - { name: '0.aura.ldpServerId', value: entityId }, + { name: '0.aura.ldpServerId', value: fakeEntityId }, ]); }); @@ -239,4 +246,41 @@ describe('previewUtils', () => { ) ).to.be.false; }); + + it('getEntityId returns valid entity ID', async () => { + const identityData: LocalWebServerIdentityData = { + identityToken: fakeIdentityToken, + usernameToServerEntityIdMap: {}, + }; + identityData.usernameToServerEntityIdMap[username] = fakeEntityId; + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(identityData); + + const entityId = await PreviewUtils.getEntityId(username); + + expect(entityId).to.equal(fakeEntityId); + }); + + it('getEntityId throws when valid data does not exist', async () => { + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(undefined); + + try { + await PreviewUtils.getEntityId(username); + } catch (err) { + expect(err).to.be.an('error').with.property('message', messages.getMessage('error.identitydata')); + } + }); + + it('getEntityId throws when entity ID does not exist', async () => { + const identityData: LocalWebServerIdentityData = { + identityToken: fakeIdentityToken, + usernameToServerEntityIdMap: {}, + }; + $$.SANDBOX.stub(ConfigUtils, 'getIdentityData').resolves(identityData); + + try { + await PreviewUtils.getEntityId(username); + } catch (err) { + expect(err).to.be.an('error').with.property('message', messages.getMessage('error.identitydata.entityid')); + } + }); });