Skip to content

Commit 93b8300

Browse files
committed
chore: improve NUTs
1 parent 662866a commit 93b8300

File tree

4 files changed

+125
-37
lines changed

4 files changed

+125
-37
lines changed

src/commands/org/open.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class OrgOpenCommand extends OrgOpenCommandBase<OrgOpenOutput> {
4545
summary: messages.getMessage('flags.path.summary'),
4646
env: 'FORCE_OPEN_URL',
4747
exclusive: ['source-file'],
48-
parse: (input: string): Promise<string> => Promise.resolve(encodeURIComponent(decodeURIComponent(input))),
4948
}),
5049
'url-only': Flags.boolean({
5150
char: 'r',

test/nut/listAndDisplay.nut.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { expect, config, assert } from 'chai';
1111
import { TestSession } from '@salesforce/cli-plugins-testkit';
1212
import { execCmd } from '@salesforce/cli-plugins-testkit';
1313
import { OrgListResult, defaultHubEmoji, defaultOrgEmoji } from '../../src/commands/org/list.js';
14-
import { OrgDisplayReturn, OrgOpenOutput } from '../../src/shared/orgTypes.js';
14+
import { OrgDisplayReturn } from '../../src/shared/orgTypes.js';
1515

1616
let hubOrgUsername: string;
1717
config.truncateThreshold = 0;
@@ -47,8 +47,6 @@ describe('Org Command NUT', () => {
4747
let session: TestSession;
4848
let defaultUsername: string;
4949
let aliasedUsername: string;
50-
let defaultUserOrgId: string;
51-
let aliasUserOrgId: string;
5250

5351
before(async () => {
5452
session = await TestSession.create({
@@ -78,10 +76,8 @@ describe('Org Command NUT', () => {
7876
assert(aliasOrg?.orgId);
7977

8078
defaultUsername = defaultOrg.username;
81-
defaultUserOrgId = defaultOrg.orgId;
8279

8380
aliasedUsername = aliasOrg?.username;
84-
aliasUserOrgId = aliasOrg?.orgId;
8581
});
8682

8783
after(async () => {
@@ -192,28 +188,4 @@ describe('Org Command NUT', () => {
192188
expect(usernameLine).to.include(aliasedUsername);
193189
});
194190
});
195-
describe('Org Open', () => {
196-
it('should produce the URL for an org in json', () => {
197-
const result = execCmd<OrgOpenOutput>(`org:open -o ${defaultUsername} --url-only --json`, {
198-
ensureExitCode: 0,
199-
}).jsonOutput?.result;
200-
expect(result).to.be.ok;
201-
expect(result).to.include({ orgId: defaultUserOrgId, username: defaultUsername });
202-
});
203-
it('should produce the URL with given path for an org in json', () => {
204-
const result = execCmd<OrgOpenOutput>(
205-
// see WI W-12694761 for single-quote behavior
206-
// eslint-disable-next-line sf-plugin/no-execcmd-double-quotes
207-
`force:org:open -o ${aliasedUsername} --urlonly --path "foo/bar/baz" --json`,
208-
{
209-
ensureExitCode: 0,
210-
}
211-
).jsonOutput?.result;
212-
expect(result).to.be.ok;
213-
expect(result).to.include({ orgId: aliasUserOrgId, username: aliasedUsername });
214-
expect(result)
215-
.to.property('url')
216-
.to.include(`startURL=${encodeURIComponent(decodeURIComponent('foo/bar/baz'))}`);
217-
});
218-
});
219191
});

test/nut/open.nut.ts

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit';
1111
import { expect, config, assert } from 'chai';
1212
import { AuthFields } from '@salesforce/core';
1313
import { ComponentSetBuilder } from '@salesforce/source-deploy-retrieve';
14+
import { ensureString } from '@salesforce/ts-types';
1415
import { OrgOpenOutput } from '../../src/shared/orgTypes.js';
1516

1617
let session: TestSession;
1718
let defaultUsername: string;
1819
let defaultUserOrgId: string;
20+
let defaultOrgInstanceUrl: string;
1921

2022
config.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: /^\/visualEditor\/appBuilder\.app\?pageId=[a-zA-Z0-9]{15,18}$/,
73+
shouldContain: ['/visualEditor/appBuilder.app', 'pageId='],
74+
idPattern: /pageId=([a-zA-Z0-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: /^\/builder_platform_interaction\/flowBuilder\.app\?flowId=[a-zA-Z0-9]{15,18}$/,
90+
shouldContain: ['flowBuilder.app', 'flowId='],
91+
idPattern: /flowId=([a-zA-Z0-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(/\.salesforce\.com$/);
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+
}

test/unit/org/open.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('org:open', () => {
2929
const testPath = '/lightning/whatever';
3030
const singleUseToken = (Math.random() + 1).toString(36).substring(2); // random string to simulate a single-use token
3131
const expectedDefaultSingleUseUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?otp=${singleUseToken}`;
32-
const expectedSingleUseUrlWithRedirect = `${expectedDefaultSingleUseUrl}&startURL=${encodeURIComponent(testPath)}`;
32+
const expectedSingleUseUrlWithRedirect = `${expectedDefaultSingleUseUrl}&startURL=${testPath}`;
3333

3434
let sfCommandUxStubs: ReturnType<typeof stubSfCommandUx>;
3535

0 commit comments

Comments
 (0)