From f7328a27baf903a2995a70891ef23371280ca032 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 5 Jun 2024 15:04:05 -0600 Subject: [PATCH] fix: add a did you mean action to AuthInfo --- package.json | 4 +++- src/org/authInfo.ts | 12 +++++++++- src/util/findSuggestion.ts | 25 +++++++++++++++++++++ test/unit/org/authInfoTest.ts | 7 ++++++ test/unit/util/findSuggestion.test.ts | 32 +++++++++++++++++++++++++++ yarn.lock | 19 +++++++++++++++- 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/util/findSuggestion.ts create mode 100644 test/unit/util/findSuggestion.test.ts diff --git a/package.json b/package.json index 7cde313cd..40b275f52 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,12 @@ ], "dependencies": { "@jsforce/jsforce-node": "^3.2.0", - "@salesforce/kit": "^3.1.1", + "@salesforce/kit": "^3.1.2", "@salesforce/schemas": "^1.9.0", "@salesforce/ts-types": "^2.0.9", "ajv": "^8.15.0", "change-case": "^4.1.2", + "fast-levenshtein": "^3.0.0", "faye": "^1.4.0", "form-data": "^4.0.0", "js2xmlparser": "^4.0.1", @@ -75,6 +76,7 @@ "@salesforce/ts-sinon": "^1.4.19", "@types/benchmark": "^2.1.5", "@types/chai-string": "^1.4.5", + "@types/fast-levenshtein": "^0.0.4", "@types/jsonwebtoken": "9.0.6", "@types/proper-lockfile": "^4.1.4", "@types/semver": "^7.5.8", diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index a70f67069..39ee26f1f 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -38,6 +38,7 @@ import { StateAggregator } from '../stateAggregator/stateAggregator'; import { filterSecrets } from '../logger/filters'; import { Messages } from '../messages'; import { getLoginAudienceCombos, SfdcUrl } from '../util/sfdcUrl'; +import { findSuggestion } from '../util/findSuggestion'; import { Connection, SFDX_HTTP_HEADERS } from './connection'; import { OrgConfigProperties } from './orgConfigProperties'; import { Org, SandboxFields } from './org'; @@ -813,7 +814,16 @@ export class AuthInfo extends AsyncOptionalCreatable { } // If a username with NO oauth options, ensure authorization already exist. else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) { - throw messages.createError('namedOrgNotFound', [username]); + throw SfError.create({ + name: 'NamedOrgNotFoundError', + message: messages.getMessage('namedOrgNotFound', [username]), + actions: [ + `It looks like you mistyped the username or alias. Did you mean "${findSuggestion(username, [ + ...(await this.stateAggregator.orgs.list()).map((f) => f.split('.json')[0]), + ...Object.keys(this.stateAggregator.aliases.getAll()), + ])}"?`, + ], + }); } else { await this.initAuthOptions(authOptions); } diff --git a/src/util/findSuggestion.ts b/src/util/findSuggestion.ts new file mode 100644 index 000000000..20cc3aa32 --- /dev/null +++ b/src/util/findSuggestion.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import Levenshtein from 'fast-levenshtein'; + +/** + * From the haystack, will find the closest value to the needle + * + * @param needle - what the user provided - find results similar to this + * @param haystack - possible results to search against + */ +export const findSuggestion = (needle: string, haystack: string[]): string => { + // we'll use this array to keep track of which piece of hay is the closest to the users entered value. + // keys closer to the index 0 will be a closer guess than keys indexed further from 0 + // an entry at 0 would be a direct match, an entry at 1 would be a single character off, etc. + const index: string[] = []; + haystack.map((hay) => { + index[Levenshtein.get(needle, hay)] = hay; + }); + + return index.find((item) => item !== undefined) ?? ''; +}; diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfoTest.ts index 0a25205d5..44b4a1556 100644 --- a/test/unit/org/authInfoTest.ts +++ b/test/unit/org/authInfoTest.ts @@ -31,6 +31,8 @@ import { OrgAccessor } from '../../../src/stateAggregator/accessors/orgAccessor' import { Crypto } from '../../../src/crypto/crypto'; import { Config } from '../../../src/config/config'; import { SfdcUrl } from '../../../src/util/sfdcUrl'; +import * as suggestion from '../../../src/util/findSuggestion'; +import { SfError } from '../../../src'; class AuthInfoMockOrg extends MockTestOrgData { public privateKey = 'authInfoTest/jwt/server.key'; @@ -2083,11 +2085,16 @@ describe('AuthInfo No fs mock', () => { it('invalid devhub username', async () => { const expectedErrorName = 'NamedOrgNotFoundError'; + stubMethod($$.SANDBOX, suggestion, 'findSuggestion').returns('doe_not_exist@gb.com'); try { await shouldThrow(AuthInfo.create({ username: 'does_not_exist@gb.com', isDevHub: true })); } catch (e) { expect(e).to.have.property('name', expectedErrorName); expect(e).to.have.property('message', 'No authorization information found for does_not_exist@gb.com.'); + expect(e).to.have.property('actions'); + expect((e as SfError).actions).to.deep.equal([ + 'It looks like you mistyped the username or alias. Did you mean "doe_not_exist@gb.com"?', + ]); } }); }); diff --git a/test/unit/util/findSuggestion.test.ts b/test/unit/util/findSuggestion.test.ts new file mode 100644 index 000000000..36fd44666 --- /dev/null +++ b/test/unit/util/findSuggestion.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * 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 { findSuggestion } from '../../../src/util/findSuggestion'; + +describe('findSuggestion Unit Tests', () => { + it('will find one suggestion', () => { + const res = findSuggestion('needl', ['haystack', 'needle']); + expect(res).to.equal('needle'); + }); + + it('will return empty string when no haystack', () => { + const res = findSuggestion('needl', []); + expect(res).to.equal(''); + }); + + it('will return last closest result', () => { + // j-k-l-m-n + // 'needl' should be right between 'needk' and 'needm' - but we found 'needm' last, which overwrites 'needk' + const res = findSuggestion('needl', ['needk', 'needm']); + expect(res).to.equal('needm'); + }); + + it('will find closest result', () => { + const res = findSuggestion('a', ['z', 'x', 'y']); + expect(res).to.equal('y'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 81d003514..0f10050ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -568,7 +568,7 @@ typescript "^5.4.3" wireit "^0.14.4" -"@salesforce/kit@^3.1.1": +"@salesforce/kit@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@salesforce/kit/-/kit-3.1.2.tgz#270741c54c70969df19ef17f8979b4ef1fa664b2" integrity sha512-si+ddvZDgx9q5czxAANuK5xhz3pv+KGspQy1wyia/7HDPKadA0QZkLTzUnO1Ju4Mux32CNHEb2y9lw9jj+eVTA== @@ -696,6 +696,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.16.tgz#b1572967f0b8b60bf3f87fe1d854a5604ea70c82" integrity sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ== +"@types/fast-levenshtein@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@types/fast-levenshtein/-/fast-levenshtein-0.0.4.tgz#a16ff6607189edf08ac631e54b5774b0faf12d87" + integrity sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA== + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -2229,6 +2234,13 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-levenshtein@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz#37b899ae47e1090e40e3fd2318e4d5f0142ca912" + integrity sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ== + dependencies: + fastest-levenshtein "^1.0.7" + fast-redact@^3.1.1: version "3.5.0" resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" @@ -2244,6 +2256,11 @@ fast-uri@^2.3.0: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.3.0.tgz#bdae493942483d299e7285dcb4627767d42e2793" integrity sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw== +fastest-levenshtein@^1.0.7: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"