@@ -11,11 +11,13 @@ import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit';
1111import { expect , config , assert } from 'chai' ;
1212import { AuthFields } from '@salesforce/core' ;
1313import { ComponentSetBuilder } from '@salesforce/source-deploy-retrieve' ;
14+ import { ensureString } from '@salesforce/ts-types' ;
1415import { OrgOpenOutput } from '../../src/shared/orgTypes.js' ;
1516
1617let session : TestSession ;
1718let defaultUsername : string ;
1819let defaultUserOrgId : string ;
20+ let defaultOrgInstanceUrl : string ;
1921
2022config . truncateThreshold = 0 ;
2123
@@ -41,6 +43,7 @@ describe('test org:open command', () => {
4143 const defaultOrg = session . orgs . get ( 'default' ) as AuthFields ;
4244 defaultUsername = defaultOrg . username as string ;
4345 defaultUserOrgId = defaultOrg . orgId as string ;
46+ defaultOrgInstanceUrl = defaultOrg . instanceUrl as string ;
4447 } ) ;
4548
4649 it ( 'should produce the frontdoor default URL for a flexipage resource when it not in org in json' , ( ) => {
@@ -49,7 +52,7 @@ describe('test org:open command', () => {
4952 } ) . jsonOutput ?. result ;
5053 assert ( result ) ;
5154 expect ( result ) . to . include ( { orgId : defaultUserOrgId , username : defaultUsername } ) ;
52- expect ( result . url ) . to . include ( 'secur/frontdoor.jsp' ) ;
55+ validateFrontdoorUrl ( result . url , undefined , defaultOrgInstanceUrl ) ;
5356 } ) ;
5457
5558 it ( 'should produce the URL for a flexipage resource in json' , async ( ) => {
@@ -63,7 +66,15 @@ describe('test org:open command', () => {
6366 } ) . jsonOutput ?. result ;
6467 assert ( result ) ;
6568 expect ( result ) . to . include ( { orgId : defaultUserOrgId , username : defaultUsername } ) ;
66- expect ( result . url ) . to . include ( 'secur/frontdoor.jsp' ) ;
69+ validateFrontdoorUrl (
70+ result . url ,
71+ {
72+ pattern : / ^ \/ v i s u a l E d i t o r \/ a p p B u i l d e r \. a p p \? p a g e I d = [ a - z A - Z 0 - 9 ] { 15 , 18 } $ / ,
73+ shouldContain : [ '/visualEditor/appBuilder.app' , 'pageId=' ] ,
74+ idPattern : / p a g e I d = ( [ a - z A - Z 0 - 9 ] { 15 , 18 } ) / ,
75+ } ,
76+ defaultOrgInstanceUrl
77+ ) ;
6778 } ) ;
6879
6980 it ( 'should produce the URL for an existing flow' , ( ) => {
@@ -72,7 +83,15 @@ describe('test org:open command', () => {
7283 } ) . jsonOutput ?. result ;
7384 assert ( result ) ;
7485 expect ( result ) . to . include ( { orgId : defaultUserOrgId , username : defaultUsername } ) ;
75- expect ( result . url ) . to . include ( 'secur/frontdoor.jsp' ) ;
86+ validateFrontdoorUrl (
87+ result . url ,
88+ {
89+ pattern : / ^ \/ b u i l d e r _ p l a t f o r m _ i n t e r a c t i o n \/ f l o w B u i l d e r \. a p p \? f l o w I d = [ a - z A - Z 0 - 9 ] { 15 , 18 } $ / ,
90+ shouldContain : [ 'flowBuilder.app' , 'flowId=' ] ,
91+ idPattern : / f l o w I d = ( [ a - z A - Z 0 - 9 ] { 15 , 18 } ) / ,
92+ } ,
93+ defaultOrgInstanceUrl
94+ ) ;
7695 } ) ;
7796
7897 it ( "should produce the org's frontdoor url when edition of file is not supported" , async ( ) => {
@@ -87,16 +106,39 @@ describe('test org:open command', () => {
87106 } ) . jsonOutput ?. result ;
88107 assert ( result ) ;
89108 expect ( result ) . to . include ( { orgId : defaultUserOrgId , username : defaultUsername } ) ;
90- expect ( result . url ) . to . include ( 'secur/frontdoor.jsp' ) ;
109+ validateFrontdoorUrl (
110+ result . url ,
111+ {
112+ exactMatch : '/lightning/setup/FlexiPageList/home' ,
113+ } ,
114+ defaultOrgInstanceUrl
115+ ) ;
91116 } ) ;
92117
93- it ( 'org: open command ' , ( ) => {
118+ it ( 'should produce the frontdoor URL to open the setup home ' , ( ) => {
94119 const result = execCmd < OrgOpenOutput > ( 'force:org:open --urlonly --json' , {
95120 ensureExitCode : 0 ,
96121 } ) . jsonOutput ?. result ;
97122 assert ( result ) ;
98123 expect ( result ) . to . have . keys ( [ 'url' , 'orgId' , 'username' ] ) ;
99- expect ( result ?. url ) . to . include ( '/secur/frontdoor.jsp' ) ;
124+ validateFrontdoorUrl ( result . url , undefined , defaultOrgInstanceUrl ) ;
125+ } ) ;
126+
127+ it ( 'should properly encode path parameters with slashes' , ( ) => {
128+ const testPath = 'lightning/setup/AsyncApiJobStatus/home' ;
129+ const result = execCmd < OrgOpenOutput > ( `force:org:open --path "${ testPath } " --urlonly --json` , {
130+ ensureExitCode : 0 ,
131+ } ) . jsonOutput ?. result ;
132+ assert ( result ) ;
133+ expect ( result ) . to . include ( { orgId : defaultUserOrgId , username : defaultUsername } ) ;
134+ // The path should be single URL encoded (foo%2Fbar%2Fbaz), not double encoded
135+ validateFrontdoorUrl (
136+ result . url ,
137+ {
138+ exactMatch : 'lightning/setup/AsyncApiJobStatus/home' ,
139+ } ,
140+ defaultOrgInstanceUrl
141+ ) ;
100142 } ) ;
101143
102144 after ( async ( ) => {
@@ -108,3 +150,78 @@ describe('test org:open command', () => {
108150 }
109151 } ) ;
110152} ) ;
153+
154+ type StartUrlValidationOptions = {
155+ pattern ?: RegExp ;
156+ exactMatch ?: string ;
157+ shouldContain ?: string [ ] ;
158+ shouldStartWith ?: string ;
159+ shouldEndWith ?: string ;
160+ idPattern ?: RegExp ; // For extracting and validating Salesforce IDs
161+ } ;
162+
163+ // Utility function to escape special regex characters
164+ function escapeRegExp ( string : string ) : string {
165+ return string . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
166+ }
167+
168+ // Enhanced helper function to validate frontdoor URLs with flexible start URL validation
169+ function validateFrontdoorUrl (
170+ urlString : string ,
171+ startUrlOptions ?: StartUrlValidationOptions ,
172+ expectedInstanceUrl ?: string
173+ ) : void {
174+ const url = new URL ( urlString ) ;
175+
176+ // Validate instance URL if provided
177+ if ( expectedInstanceUrl ) {
178+ const instanceUrl = new URL ( expectedInstanceUrl ) ;
179+ expect ( url . hostname ) . to . equal ( instanceUrl . hostname ) ;
180+ } else {
181+ // Validate it's a Salesforce domain
182+ expect ( url . hostname ) . to . match ( / \. s a l e s f o r c e \. c o m $ / ) ;
183+ }
184+
185+ // Validate it's the frontdoor endpoint
186+ expect ( url . pathname ) . to . equal ( '/secur/frontdoor.jsp' ) ;
187+
188+ // Validate required query parameters
189+ expect ( url . searchParams . has ( 'otp' ) ) . to . be . true ;
190+ expect ( url . searchParams . has ( 'cshc' ) ) . to . be . true ;
191+
192+ if ( startUrlOptions ) {
193+ const actualStartUrl = url . searchParams . get ( 'startURL' ) ;
194+ expect ( actualStartUrl ) . to . not . be . null ;
195+
196+ const decodedStartUrl = decodeURIComponent ( ensureString ( actualStartUrl ) ) ;
197+
198+ if ( startUrlOptions . exactMatch ) {
199+ expect ( decodedStartUrl ) . to . equal ( startUrlOptions . exactMatch ) ;
200+ }
201+
202+ if ( startUrlOptions . pattern ) {
203+ expect ( decodedStartUrl ) . to . match ( startUrlOptions . pattern ) ;
204+ }
205+
206+ if ( startUrlOptions . shouldContain ) {
207+ startUrlOptions . shouldContain . forEach ( ( substring ) => {
208+ expect ( decodedStartUrl ) . to . include ( substring ) ;
209+ } ) ;
210+ }
211+
212+ if ( startUrlOptions . shouldStartWith ) {
213+ expect ( decodedStartUrl ) . to . match ( new RegExp ( `^${ escapeRegExp ( startUrlOptions . shouldStartWith ) } ` ) ) ;
214+ }
215+
216+ if ( startUrlOptions . shouldEndWith ) {
217+ expect ( decodedStartUrl ) . to . match ( new RegExp ( `${ escapeRegExp ( startUrlOptions . shouldEndWith ) } $` ) ) ;
218+ }
219+
220+ if ( startUrlOptions . idPattern ) {
221+ expect ( decodedStartUrl ) . to . match ( startUrlOptions . idPattern ) ;
222+ }
223+ } else {
224+ // If no startURL options provided, expect no startURL parameter
225+ expect ( url . searchParams . get ( 'startURL' ) ) . to . be . null ;
226+ }
227+ }
0 commit comments