From 876db34a3210a38b20c888cabe317b8fa633a98b Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 4 Aug 2025 14:13:35 -0700 Subject: [PATCH 1/6] fix: remove the warnings and update frontdoorUrl method --- messages/messages.md | 4 -- src/commands/org/open.ts | 69 +++++++------------------------- src/commands/org/open/agent.ts | 2 +- src/shared/orgOpenCommandBase.ts | 41 ++----------------- src/shared/orgOpenUtils.ts | 29 ++------------ test/unit/org/open.test.ts | 6 +-- 6 files changed, 23 insertions(+), 128 deletions(-) diff --git a/messages/messages.md b/messages/messages.md index 4d92eb90f..b01091150 100644 --- a/messages/messages.md +++ b/messages/messages.md @@ -4,10 +4,6 @@ This command will expose sensitive information that allows for subsequent activi Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information, please review the authorization section of the https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_web_flow.htm. -# BehaviorChangeWarning - -Starting in August 2025, this command will generate single-use URLs when you specify either the --json or --url-only (-r) flag. These URLs can be used only one time; subsequent use won't allow you to log in to the org. - # SingleAccessFrontdoorError Failed to generate a single-use frontdoor URL. diff --git a/src/commands/org/open.ts b/src/commands/org/open.ts index 0cbbe7367..f39203048 100644 --- a/src/commands/org/open.ts +++ b/src/commands/org/open.ts @@ -5,23 +5,20 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import path from 'node:path'; import { Flags, loglevel, orgApiVersionFlagWithDeprecations, requiredOrgFlagWithDeprecations, } from '@salesforce/sf-plugins-core'; -import { Connection, Messages } from '@salesforce/core'; +import { Messages, Org } from '@salesforce/core'; import { MetadataResolver } from '@salesforce/source-deploy-retrieve'; -import { env } from '@salesforce/kit'; import { buildFrontdoorUrl } from '../../shared/orgOpenUtils.js'; import { OrgOpenCommandBase } from '../../shared/orgOpenCommandBase.js'; import { type OrgOpenOutput } from '../../shared/orgTypes.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); -const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); export class OrgOpenCommand extends OrgOpenCommandBase { public static readonly summary = messages.getMessage('summary'); @@ -68,75 +65,39 @@ export class OrgOpenCommand extends OrgOpenCommandBase { }; public async run(): Promise { - this.warn(sharedMessages.getMessage('BehaviorChangeWarning')); const { flags } = await this.parse(OrgOpenCommand); this.org = flags['target-org']; this.connection = this.org.getConnection(flags['api-version']); - const singleUseEnvVar: boolean = env.getBoolean('SF_SINGLE_USE_ORG_OPEN_URL'); - const singleUseMode = singleUseEnvVar ? singleUseEnvVar : !(flags['url-only'] || this.jsonEnabled()); const [frontDoorUrl, retUrl] = await Promise.all([ - buildFrontdoorUrl(this.org, this.connection, singleUseMode), - flags['source-file'] ? generateFileUrl(flags['source-file'], this.connection) : flags.path, + buildFrontdoorUrl(this.org), + flags['source-file'] ? generateFileUrl(flags['source-file'], this.org) : flags.path, ]); return this.openOrgUI(flags, frontDoorUrl, retUrl); } } -const generateFileUrl = async (file: string, conn: Connection): Promise => { +const generateFileUrl = async (file: string, org: Org): Promise => { try { const metadataResolver = new MetadataResolver(); const components = metadataResolver.getComponentsFromPath(file); const typeName = components[0]?.type?.name; - switch (typeName) { - case 'Bot': - return `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${await botFileNameToId(conn, file)}`; - case 'ApexPage': - return `/apex/${path.basename(file).replace('.page-meta.xml', '').replace('.page', '')}`; - case 'Flow': - return `/builder_platform_interaction/flowBuilder.app?flowId=${await flowFileNameToId(conn, file)}`; - case 'FlexiPage': - return `/visualEditor/appBuilder.app?pageId=${await flexiPageFilenameToId(conn, file)}`; - default: - return 'lightning/setup/FlexiPageList/home'; + if (!typeName) { + throw new Error(`Unable to determine metadata type for file: ${file}`); } + + return await org.getMetadataUIURL(typeName, file); } catch (error) { - if (error instanceof Error && error.name === 'FlowIdNotFoundError') { + if ( + error instanceof Error && + (error.message.includes('FlowIdNotFound') || + error.message.includes('CustomObjectIdNotFound') || + error.message.includes('ApexClassIdNotFound')) + ) { throw error; } - return 'lightning/setup/FlexiPageList/home'; - } -}; - -const botFileNameToId = async (conn: Connection, filePath: string): Promise => - ( - await conn.singleRecordQuery<{ Id: string }>( - `SELECT id FROM BotDefinition WHERE DeveloperName='${path.basename(filePath, '.bot-meta.xml')}'` - ) - ).Id; - -/** query flexipage via toolingPAI to get its ID (starts with 0M0) */ -const flexiPageFilenameToId = async (conn: Connection, filePath: string): Promise => - ( - await conn.singleRecordQuery<{ Id: string }>( - `SELECT id FROM flexipage WHERE DeveloperName='${path.basename(filePath, '.flexipage-meta.xml')}'`, - { tooling: true } - ) - ).Id; - -/** query the rest API to turn a flow's filepath into a FlowId (starts with 301) */ -const flowFileNameToId = async (conn: Connection, filePath: string): Promise => { - try { - const flow = await conn.singleRecordQuery<{ DurableId: string }>( - `SELECT DurableId FROM FlowVersionView WHERE FlowDefinitionView.ApiName = '${path.basename( - filePath, - '.flow-meta.xml' - )}' ORDER BY VersionNumber DESC LIMIT 1` - ); - return flow.DurableId; - } catch (error) { - throw messages.createError('FlowIdNotFound', [filePath]); + throw new Error(`Unable to generate metadata URL for file: ${file}`); } }; diff --git a/src/commands/org/open/agent.ts b/src/commands/org/open/agent.ts index 88b5781ec..788116e26 100644 --- a/src/commands/org/open/agent.ts +++ b/src/commands/org/open/agent.ts @@ -52,7 +52,7 @@ export class OrgOpenAgent extends OrgOpenCommandBase { this.connection = this.org.getConnection(flags['api-version']); const [frontDoorUrl, retUrl] = await Promise.all([ - buildFrontdoorUrl(this.org, this.connection, true), + buildFrontdoorUrl(this.org), buildRetUrl(this.connection, flags['api-name']), ]); diff --git a/src/shared/orgOpenCommandBase.ts b/src/shared/orgOpenCommandBase.ts index 953c7939b..342a773d3 100644 --- a/src/shared/orgOpenCommandBase.ts +++ b/src/shared/orgOpenCommandBase.ts @@ -5,16 +5,12 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import path from 'node:path'; -import fs from 'node:fs'; -import { platform, tmpdir } from 'node:os'; -import { execSync } from 'node:child_process'; -import isWsl from 'is-wsl'; +import { platform } from 'node:os'; import { apps } from 'open'; import { SfCommand } from '@salesforce/sf-plugins-core'; import { Connection, Messages, Org, SfdcUrl, SfError } from '@salesforce/core'; -import { env, sleep } from '@salesforce/kit'; -import utils, { fileCleanup, getFileContents, handleDomainError } from './orgOpenUtils.js'; +import { env } from '@salesforce/kit'; +import utils, { handleDomainError } from './orgOpenUtils.js'; import { type OrgOpenOutput } from './orgTypes.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); @@ -77,37 +73,6 @@ export abstract class OrgOpenCommandBase extends SfCommand { } if (this.jsonEnabled()) { - // TODO: remove this code path once the org open behavior changes on August 2025 (see W-17661469) - // create a local html file that contains the POST stuff. - const tempFilePath = path.join(tmpdir(), `org-open-${new Date().valueOf()}.html`); - await fs.promises.writeFile( - tempFilePath, - getFileContents( - this.connection.accessToken as string, - this.connection.instanceUrl, - // the path flag is URI-encoded in its `parse` func. - // For the form redirect to work we need it decoded. - flags.path ? decodeURIComponent(flags.path) : retUrl - ) - ); - const filePathUrl = isWsl - ? 'file:///' + execSync(`wslpath -m ${tempFilePath}`).toString().trim() - : `file:///${tempFilePath}`; - const cp = await utils.openUrl(filePathUrl, { - ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), - ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), - }); - cp.on('error', (err) => { - fileCleanup(tempFilePath); - throw SfError.wrap(err); - }); - // so we don't delete the file while the browser is still using it - // open returns when the CP is spawned, but there's not way to know if the browser is still using the file - await sleep(platform() === 'win32' || isWsl ? 7000 : 5000); - fileCleanup(tempFilePath); - } else { - // it means we generated a one-time use frontdoor url - // so the workaround to create a local html file is not needed const cp = await utils.openUrl(url, { ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), diff --git a/src/shared/orgOpenUtils.ts b/src/shared/orgOpenUtils.ts index 559be34ce..55a3b3205 100644 --- a/src/shared/orgOpenUtils.ts +++ b/src/shared/orgOpenUtils.ts @@ -8,20 +8,17 @@ import { rmSync } from 'node:fs'; import { ChildProcess } from 'node:child_process'; import open, { Options } from 'open'; -import { Connection, Logger, Messages, Org, SfError } from '@salesforce/core'; +import { Logger, Messages, Org, SfError } from '@salesforce/core'; import { Duration, Env } from '@salesforce/kit'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); -const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); export const openUrl = async (url: string, options: Options): Promise => open(url, options); export const fileCleanup = (tempFilePath: string): void => rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true }); -type SingleAccessUrlRes = { frontdoor_uri: string | undefined }; - /** * This method generates and returns a frontdoor url for the given org. * @@ -29,28 +26,8 @@ type SingleAccessUrlRes = { frontdoor_uri: string | undefined }; * @param conn the Connection for the given Org. * @param singleUseUrl if true returns a single-use url frontdoor url. */ -export const buildFrontdoorUrl = async (org: Org, conn: Connection, singleUseUrl: boolean): Promise => { - await org.refreshAuth(); // we need a live accessToken for the frontdoor url - const accessToken = conn.accessToken; - if (!accessToken) { - throw new SfError('NoAccessToken', 'NoAccessToken'); - } - if (singleUseUrl) { - try { - const response: SingleAccessUrlRes = await conn.requestGet('/services/oauth2/singleaccess'); - if (response.frontdoor_uri) return response.frontdoor_uri; - throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError')).setData(response); - } catch (e) { - if (e instanceof SfError) throw e; - const err = e as Error; - throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError'), err.message); - } - } else { - // TODO: remove this code path once the org open behavior changes on August 2025 (see W-17661469) - const instanceUrlClean = org.getField(Org.Fields.INSTANCE_URL).replace(/\/$/, ''); - return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`; - } -}; + +export const buildFrontdoorUrl = async (org: Org): Promise => org.getFrontDoorUrl(); export const handleDomainError = (err: unknown, url: string, env: Env): string => { if (err instanceof Error) { diff --git a/test/unit/org/open.test.ts b/test/unit/org/open.test.ts index fb2cee38a..6943672d6 100644 --- a/test/unit/org/open.test.ts +++ b/test/unit/org/open.test.ts @@ -177,15 +177,13 @@ describe('org:open', () => { expect(spies.get('requestGet').args[0][0]).to.deep.equal('/services/oauth2/singleaccess'); }); - it('honors SF_SINGLE_USE_ORG_OPEN_URL env var and generates a single-use frontdoor url even if --url-only or --json flag are passed in', async () => { - process.env.SF_SINGLE_USE_ORG_OPEN_URL = 'true'; + it('generates a single-use frontdoor url even if --url-only or --json flag are passed in', async () => { spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); const response = await OrgOpenCommand.run(['--targetusername', testOrg.username, '--json', '--url-only']); expect(response.url).to.equal(expectedDefaultSingleUseUrl); // verify we called to the correct endpoint to generate the single-use AT expect(spies.get('requestGet').callCount).to.equal(1); expect(spies.get('requestGet').args[0][0]).to.deep.equal('/services/oauth2/singleaccess'); - delete process.env.SF_SINGLE_USE_ORG_OPEN_URL; }); it('handles api error', async () => { @@ -322,7 +320,6 @@ describe('org:open', () => { ) ); expect(sfCommandUxStubs.warn.calledOnceWith(sharedMessages.getMessage('SecurityWarning'))); - expect(sfCommandUxStubs.warn.calledOnceWith(sharedMessages.getMessage('BehaviorChangeWarning'))); expect(spies.get('resolver').callCount).to.equal(1); expect(spies.get('open').callCount).to.equal(1); @@ -335,7 +332,6 @@ describe('org:open', () => { await OrgOpenCommand.run(['--targetusername', testOrg.username, '--path', testPath, '-b', testBrowser]); expect(sfCommandUxStubs.warn(sharedMessages.getMessage('SecurityWarning'))); - expect(sfCommandUxStubs.warn.calledOnceWith(sharedMessages.getMessage('BehaviorChangeWarning'))); expect( sfCommandUxStubs.logSuccess.calledOnceWith( messages.getMessage('humanSuccessNoUrl', [testOrg.orgId, testOrg.username]) From ecc5136895dde9cd2dbec0afb4fe0bf3334719d1 Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 4 Aug 2025 20:43:01 -0700 Subject: [PATCH 2/6] fix: update UT using frontdoorURL --- messages/messages.md | 2 +- src/shared/orgOpenUtils.ts | 13 ++++-- test/unit/org/open.test.ts | 86 +++++++++++++++++++++++--------------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/messages/messages.md b/messages/messages.md index b01091150..7119b54ed 100644 --- a/messages/messages.md +++ b/messages/messages.md @@ -6,4 +6,4 @@ For additional information, please review the authorization section of the https # SingleAccessFrontdoorError -Failed to generate a single-use frontdoor URL. +Failed to generate a frontdoor URL. diff --git a/src/shared/orgOpenUtils.ts b/src/shared/orgOpenUtils.ts index 55a3b3205..fa20ca6b0 100644 --- a/src/shared/orgOpenUtils.ts +++ b/src/shared/orgOpenUtils.ts @@ -13,6 +13,7 @@ import { Duration, Env } from '@salesforce/kit'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); +const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); export const openUrl = async (url: string, options: Options): Promise => open(url, options); @@ -23,11 +24,17 @@ export const fileCleanup = (tempFilePath: string): void => * This method generates and returns a frontdoor url for the given org. * * @param org org for which we generate the frontdoor url. - * @param conn the Connection for the given Org. - * @param singleUseUrl if true returns a single-use url frontdoor url. */ -export const buildFrontdoorUrl = async (org: Org): Promise => org.getFrontDoorUrl(); +export const buildFrontdoorUrl = async (org: Org): Promise => { + try { + return await org.getFrontDoorUrl(); + } catch (e) { + if (e instanceof SfError) throw e; + const err = e as Error; + throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError'), err.message); + } +}; export const handleDomainError = (err: unknown, url: string, env: Env): string => { if (err instanceof Error) { diff --git a/test/unit/org/open.test.ts b/test/unit/org/open.test.ts index 6943672d6..9943c58bd 100644 --- a/test/unit/org/open.test.ts +++ b/test/unit/org/open.test.ts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import { join } from 'node:path'; import * as process from 'node:process'; import { assert, expect } from 'chai'; -import { Connection, Messages, SfdcUrl, SfError } from '@salesforce/core'; +import { Connection, Messages, Org, SfdcUrl, SfError } from '@salesforce/core'; import { stubMethod } from '@salesforce/ts-sinon'; import { MockTestOrgData, shouldThrow, TestContext } from '@salesforce/core/testSetup'; import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core'; @@ -27,10 +27,9 @@ describe('org:open', () => { const testBrowser = 'firefox'; const testPath = '/lightning/whatever'; - const expectedDefaultUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?sid=${testOrg.accessToken}`; - const expectedUrl = `${expectedDefaultUrl}&retURL=${encodeURIComponent(testPath)}`; const singleUseToken = (Math.random() + 1).toString(36).substring(2); // random string to simulate a single-use token const expectedDefaultSingleUseUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?otp=${singleUseToken}`; + const expectedSingleUseUrl = `${expectedDefaultSingleUseUrl}&startURL=${encodeURIComponent(testPath)}`; let sfCommandUxStubs: ReturnType; @@ -66,7 +65,7 @@ describe('org:open', () => { const response = await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--urlonly']); assert(response); testJsonStructure(response); - expect(response.url).to.equal(expectedDefaultUrl); + expect(response.url).to.equal(expectedDefaultSingleUseUrl); }); it('org with a url is built correctly', async () => { @@ -79,7 +78,7 @@ describe('org:open', () => { testPath, ]); assert(response); - expect(response.url).to.equal(expectedUrl); + expect(response.url).to.equal(expectedSingleUseUrl); }); describe('--source-file', () => { @@ -109,6 +108,8 @@ describe('org:open', () => { it('--source-file to flexipage', async () => { $$.SANDBOX.stub(Connection.prototype, 'singleRecordQuery').resolves({ Id: '123' }); + const mockMetadataUrl = 'visualEditor/appBuilder.app?pageId=123'; + $$.SANDBOX.stub(Org.prototype, 'getMetadataUIURL').resolves(mockMetadataUrl); const response = await OrgOpenCommand.run([ '--json', @@ -118,10 +119,14 @@ describe('org:open', () => { '--source-file', flexipagePath, ]); + expect(response.url).to.include('visualEditor/appBuilder.app?pageId=123'); }); it('--source-file to an ApexPage', async () => { + const mockMetadataUrl = '/apex/test'; + $$.SANDBOX.stub(Org.prototype, 'getMetadataUIURL').resolves(mockMetadataUrl); + const response = await OrgOpenCommand.run([ '--json', '--targetusername', @@ -130,31 +135,41 @@ describe('org:open', () => { '--source-file', apexPath, ]); - expect(response.url).to.include('&retURL=/apex/test'); + expect(response.url).to.include('&startURL=/apex/test'); }); it('--source-file when flexipage query errors', async () => { - const response = await OrgOpenCommand.run([ - '--json', - '--targetusername', - testOrg.username, - '--urlonly', - '--source-file', - flexipagesDir, - ]); - expect(response.url).to.include('lightning/setup/FlexiPageList/home'); + try { + await OrgOpenCommand.run([ + '--json', + '--targetusername', + testOrg.username, + '--urlonly', + '--source-file', + flexipagesDir, + ]); + expect.fail('should have thrown an error'); + } catch (e) { + assert(e instanceof Error); + expect(e.message).to.include(`Unable to generate metadata URL for file: ${flexipagesDir}`); + } }); it('--source-file to neither flexipage or apexpage', async () => { - const response = await OrgOpenCommand.run([ - '--json', - '--targetusername', - testOrg.username, - '--urlonly', - '--source-file', - apexDir, - ]); - expect(response.url).to.include('lightning/setup/FlexiPageList/home'); + try { + await OrgOpenCommand.run([ + '--json', + '--targetusername', + testOrg.username, + '--urlonly', + '--source-file', + apexDir, + ]); + expect.fail('should have thrown an error'); + } catch (e) { + assert(e instanceof Error); + expect(e.message).to.include(`Unable to generate metadata URL for file: ${apexDir}`); + } }); }); @@ -164,7 +179,8 @@ describe('org:open', () => { const response = await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--urlonly']); assert(response); testJsonStructure(response); - expect(response.url).to.equal(expectedUrl); + expect(response.url).to.equal(expectedSingleUseUrl); + expect(spies.get('requestGet').callCount).to.equal(1); delete process.env.FORCE_OPEN_URL; }); @@ -174,7 +190,7 @@ describe('org:open', () => { expect(response.url).to.equal(expectedDefaultSingleUseUrl); // verify we called to the correct endpoint to generate the single-use AT expect(spies.get('requestGet').callCount).to.equal(1); - expect(spies.get('requestGet').args[0][0]).to.deep.equal('/services/oauth2/singleaccess'); + expect(spies.get('requestGet').args[0][0]).to.deep.equal(`${testOrg.instanceUrl}/services/oauth2/singleaccess`); }); it('generates a single-use frontdoor url even if --url-only or --json flag are passed in', async () => { @@ -183,12 +199,13 @@ describe('org:open', () => { expect(response.url).to.equal(expectedDefaultSingleUseUrl); // verify we called to the correct endpoint to generate the single-use AT expect(spies.get('requestGet').callCount).to.equal(1); - expect(spies.get('requestGet').args[0][0]).to.deep.equal('/services/oauth2/singleaccess'); + expect(spies.get('requestGet').args[0][0]).to.deep.equal(`${testOrg.instanceUrl}/services/oauth2/singleaccess`); }); it('handles api error', async () => { $$.SANDBOX.restore(); const mockError = new Error('Invalid_Scope'); + mockError.name = 'Invalid_Scope'; $$.SANDBOX.stub(Connection.prototype, 'requestGet').throws(mockError); try { await OrgOpenCommand.run(['--targetusername', testOrg.username]); @@ -223,7 +240,7 @@ describe('org:open', () => { const response = await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath]); assert(response); testJsonStructure(response); - expect(response.url).to.equal(expectedUrl); + expect(response.url).to.equal(expectedSingleUseUrl); expect(spies.get('resolver').callCount).to.equal(1); }); @@ -254,7 +271,7 @@ describe('org:open', () => { const response = await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath]); assert(response); testJsonStructure(response); - expect(response.url).to.equal(expectedUrl); + expect(response.url).to.equal(expectedSingleUseUrl); expect(spies.get('resolver').callCount).to.equal(0); delete process.env.SFDX_CONTAINER_MODE; }); @@ -265,6 +282,7 @@ describe('org:open', () => { const response = await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath]); assert(response); testJsonStructure(response); + expect(response.url).to.equal(expectedSingleUseUrl); expect(spies.get('resolver').callCount).to.equal(1); delete process.env.SF_DOMAIN_RETRY; }); @@ -273,7 +291,7 @@ describe('org:open', () => { describe('human output', () => { it('calls open and outputs proper success message (no url)', async () => { spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); - await OrgOpenCommand.run(['--targetusername', testOrg.username, '--path', testPath]); + await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath]); expect(sfCommandUxStubs.logSuccess.firstCall.args).to.include( messages.getMessage('humanSuccessNoUrl', [testOrg.orgId, testOrg.username]) @@ -288,7 +306,7 @@ describe('org:open', () => { await OrgOpenCommand.run(['--targetusername', testOrg.username, '--path', testPath, '--urlonly']); expect(sfCommandUxStubs.logSuccess.firstCall.args).to.include( - messages.getMessage('humanSuccess', [testOrg.orgId, testOrg.username, expectedUrl]) + messages.getMessage('humanSuccess', [testOrg.orgId, testOrg.username, expectedSingleUseUrl]) ); }); @@ -313,7 +331,7 @@ describe('org:open', () => { it('calls open with no browser argument', async () => { spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); - await OrgOpenCommand.run(['--targetusername', testOrg.username, '--path', testPath]); + await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath]); expect( sfCommandUxStubs.logSuccess.calledOnceWith( messages.getMessage('humanSuccessNoUrl', [testOrg.orgId, testOrg.username]) @@ -329,9 +347,9 @@ describe('org:open', () => { it('calls open with a browser argument', async () => { spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); - await OrgOpenCommand.run(['--targetusername', testOrg.username, '--path', testPath, '-b', testBrowser]); + await OrgOpenCommand.run(['--json', '--targetusername', testOrg.username, '--path', testPath, '-b', testBrowser]); - expect(sfCommandUxStubs.warn(sharedMessages.getMessage('SecurityWarning'))); + expect(sfCommandUxStubs.warn.calledOnceWith(sharedMessages.getMessage('SecurityWarning'))); expect( sfCommandUxStubs.logSuccess.calledOnceWith( messages.getMessage('humanSuccessNoUrl', [testOrg.orgId, testOrg.username]) From 477e54397365495423d95edfede2a1116a52605a Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 5 Aug 2025 09:29:14 -0700 Subject: [PATCH 3/6] fix: update NUTS --- test/nut/open.nut.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/nut/open.nut.ts b/test/nut/open.nut.ts index de1e9ab6e..57ffb6ff0 100644 --- a/test/nut/open.nut.ts +++ b/test/nut/open.nut.ts @@ -43,13 +43,13 @@ describe('test org:open command', () => { defaultUserOrgId = defaultOrg.orgId as string; }); - it('should produce the default URL for a flexipage resource when it not in org in json', () => { + it('should produce the frontdoor default URL for a flexipage resource when it not in org in json', () => { const result = execCmd(`force:source:open -f ${flexiPagePath} --urlonly --json`, { ensureExitCode: 0, }).jsonOutput?.result; assert(result); expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername }); - expect(result.url).to.include('lightning/setup/FlexiPageList/home'); + expect(result.url).to.include('secur/frontdoor.jsp'); }); it('should produce the URL for a flexipage resource in json', async () => { @@ -63,7 +63,7 @@ describe('test org:open command', () => { }).jsonOutput?.result; assert(result); expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername }); - expect(result.url).to.include('/visualEditor/appBuilder.app?pageId'); + expect(result.url).to.include('secur/frontdoor.jsp'); }); it('should produce the URL for an existing flow', () => { @@ -72,7 +72,7 @@ describe('test org:open command', () => { }).jsonOutput?.result; assert(result); expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername }); - expect(result.url).to.include('/builder_platform_interaction/flowBuilder.app?flowId=301'); + expect(result.url).to.include('secur/frontdoor.jsp'); }); it("should produce the org's frontdoor url when edition of file is not supported", async () => { From 981e69421d717a73f35ad359173d25c0aa97ff6c Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 5 Aug 2025 10:13:35 -0700 Subject: [PATCH 4/6] fix: update nuts --- src/commands/org/open.ts | 2 +- test/nut/listAndDisplay.nut.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/org/open.ts b/src/commands/org/open.ts index f39203048..30b773c85 100644 --- a/src/commands/org/open.ts +++ b/src/commands/org/open.ts @@ -98,6 +98,6 @@ const generateFileUrl = async (file: string, org: Org): Promise => { ) { throw error; } - throw new Error(`Unable to generate metadata URL for file: ${file}`); + return ''; } }; diff --git a/test/nut/listAndDisplay.nut.ts b/test/nut/listAndDisplay.nut.ts index 33d139056..45a962876 100644 --- a/test/nut/listAndDisplay.nut.ts +++ b/test/nut/listAndDisplay.nut.ts @@ -213,7 +213,7 @@ describe('Org Command NUT', () => { expect(result).to.include({ orgId: aliasUserOrgId, username: aliasedUsername }); expect(result) .to.property('url') - .to.include(`retURL=${encodeURIComponent(decodeURIComponent('foo/bar/baz'))}`); + .to.include(`startURL=${encodeURIComponent(decodeURIComponent('foo/bar/baz'))}`); }); }); }); From 7ad363a866db011d25105949c72fa9ea80824853 Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 5 Aug 2025 10:21:11 -0700 Subject: [PATCH 5/6] fix: update unit tests for org open --- test/unit/org/open.test.ts | 48 ++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/test/unit/org/open.test.ts b/test/unit/org/open.test.ts index 9943c58bd..c4f52a00b 100644 --- a/test/unit/org/open.test.ts +++ b/test/unit/org/open.test.ts @@ -139,37 +139,29 @@ describe('org:open', () => { }); it('--source-file when flexipage query errors', async () => { - try { - await OrgOpenCommand.run([ - '--json', - '--targetusername', - testOrg.username, - '--urlonly', - '--source-file', - flexipagesDir, - ]); - expect.fail('should have thrown an error'); - } catch (e) { - assert(e instanceof Error); - expect(e.message).to.include(`Unable to generate metadata URL for file: ${flexipagesDir}`); - } + const response = await OrgOpenCommand.run([ + '--json', + '--targetusername', + testOrg.username, + '--urlonly', + '--source-file', + flexipagesDir, + ]); + + expect(response.url).to.equal(expectedDefaultSingleUseUrl); }); it('--source-file to neither flexipage or apexpage', async () => { - try { - await OrgOpenCommand.run([ - '--json', - '--targetusername', - testOrg.username, - '--urlonly', - '--source-file', - apexDir, - ]); - expect.fail('should have thrown an error'); - } catch (e) { - assert(e instanceof Error); - expect(e.message).to.include(`Unable to generate metadata URL for file: ${apexDir}`); - } + const response = await OrgOpenCommand.run([ + '--json', + '--targetusername', + testOrg.username, + '--urlonly', + '--source-file', + apexDir, + ]); + + expect(response.url).to.equal(expectedDefaultSingleUseUrl); }); }); From a69571f68f61793ef83ffab111f7c3e013733679 Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 5 Aug 2025 14:03:09 -0700 Subject: [PATCH 6/6] fix: remove the if statement this.jsonEnabled --- src/shared/orgOpenCommandBase.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/shared/orgOpenCommandBase.ts b/src/shared/orgOpenCommandBase.ts index 342a773d3..bd2accc74 100644 --- a/src/shared/orgOpenCommandBase.ts +++ b/src/shared/orgOpenCommandBase.ts @@ -42,8 +42,8 @@ export abstract class OrgOpenCommandBase extends SfCommand { // NOTE: Deliberate use of `||` here since getBoolean() defaults to false, and we need to consider both env vars. const containerMode = env.getBoolean('SF_CONTAINER_MODE') || env.getBoolean('SFDX_CONTAINER_MODE'); - // security warning only for --json OR --url-only OR containerMode - if (flags['url-only'] || this.jsonEnabled() || containerMode) { + // security warning only for --url-only OR containerMode + if (flags['url-only'] || containerMode) { const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); this.warn(sharedMessages.getMessage('SecurityWarning')); this.log(''); @@ -72,15 +72,13 @@ export abstract class OrgOpenCommandBase extends SfCommand { handleDomainError(err, url, env); } - if (this.jsonEnabled()) { - const cp = await utils.openUrl(url, { - ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), - ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), - }); - cp.on('error', (err) => { - throw SfError.wrap(err); - }); - } + const cp = await utils.openUrl(url, { + ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), + ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), + }); + cp.on('error', (err) => { + throw SfError.wrap(err); + }); return output; }