diff --git a/src/commands/force/user/create.ts b/src/commands/force/user/create.ts index 02aedc00..577f9bcf 100644 --- a/src/commands/force/user/create.ts +++ b/src/commands/force/user/create.ts @@ -20,6 +20,7 @@ import { UserFields, } from '@salesforce/core'; import { QueryResult } from 'jsforce'; +import { omit, mapKeys } from '@salesforce/kit'; import { getString, Dictionary, isArray } from '@salesforce/ts-types'; import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; @@ -36,6 +37,25 @@ interface FailureMsg { message: string; } +const permsetsStringToArray = (fieldsPermsets: string | string[]): string[] => { + if (!fieldsPermsets) return []; + return isArray(fieldsPermsets) + ? fieldsPermsets + : fieldsPermsets.split(',').map((item) => item.replace("'", '').trim()); +}; + +const standardizePasswordToBoolean = (input: unknown): boolean => { + if (typeof input === 'boolean') { + return input; + } + if (typeof input === 'string') { + if (input.toLowerCase() === 'true') { + return true; + } + } + return false; +}; + export class UserCreateCommand extends SfdxCommand { public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessage('examples').split(os.EOL); @@ -61,22 +81,16 @@ export class UserCreateCommand extends SfdxCommand { private authInfo: AuthInfo; /** - * removes fields that cause errors in salesforce api's within sfdx-core's createUser method + * removes fields that cause errors in salesforce APIs within sfdx-core's createUser method * * @param fields a list of combined fields from varargs and the config file * @private */ private static stripInvalidAPIFields(fields: UserFields & Dictionary): UserFields { - const copy = Object.assign({}, fields); - // remove invalid fields for userCreate() - delete copy.permsets; - delete copy.generatepassword; - delete copy.generatePassword; - delete copy.profileName; - return copy as UserFields; + return omit(fields, ['permsets', 'generatepassword', 'generatePassword', 'profileName']); } - public async run(): Promise { + public async run(): Promise { this.logger = await Logger.child(this.constructor.name); const defaultUserFields: DefaultUserFields = await DefaultUserFields.create({ templateUser: this.org.getUsername(), @@ -98,9 +112,7 @@ export class UserCreateCommand extends SfdxCommand { try { // permsets can be passed from cli args or file we need to create an array of permset names either way it's passed // it will either be a comma separated string, or an array, force it into an array - const permsetArray: string[] = isArray(fields.permsets) - ? fields.permsets - : fields.permsets.trim().split(','); + const permsetArray = permsetsStringToArray(fields.permsets); await this.user.assignPermissionSets(this.authInfo.getFields().userId, permsetArray); this.successes.push({ @@ -147,7 +159,12 @@ export class UserCreateCommand extends SfdxCommand { this.print(fields); - return Object.assign({ orgId: this.org.getOrgId() }, fields); + const { permsets, ...fieldsWithoutPermsets } = fields; + return { + orgId: this.org.getOrgId(), + permissionSetAssignments: permsetsStringToArray(permsets), + fields: { ...mapKeys(fieldsWithoutPermsets, (value, key) => key.toLowerCase()) }, + }; } private async catchCreateUser(respBody: Error, fields: UserFields): Promise { @@ -184,17 +201,22 @@ export class UserCreateCommand extends SfdxCommand { if (this.varargs) { Object.keys(this.varargs).forEach((key) => { - defaultFields[this.lowerFirstLetter(key)] = this.varargs[key]; - if (key.toLowerCase() === 'generatepassword') { - defaultFields['generatePassword'] = this.varargs[key]; + // standardize generatePassword casing + defaultFields['generatePassword'] = standardizePasswordToBoolean(this.varargs[key]); + } else if (key.toLowerCase() === 'profilename') { + // standardize profileName casing + defaultFields['profileName'] = this.varargs[key]; + } else { + // all other varargs are left "as is" + defaultFields[this.lowerFirstLetter(key)] = this.varargs[key]; } }); } // check if "profileName" was passed, this needs to become a profileId before calling User.create if (defaultFields['profileName']) { - const name = (defaultFields['profileName'] || 'Standard User') as string; + const name = (defaultFields['profileName'] ?? 'Standard User') as string; this.logger.debug(`Querying org for profile name [${name}]`); const response: QueryResult<{ Id: string }> = await this.org .getConnection() @@ -202,30 +224,6 @@ export class UserCreateCommand extends SfdxCommand { defaultFields.profileId = response.records[0].Id; } - // the file schema is camelCase and boolean while the cli arg is no capitialization and a string - // we will add logic to capture camelcase in varargs just in case - if ( - defaultFields['generatepassword'] === 'true' || - defaultFields['generatePassword'] === 'true' || - defaultFields['generatePassword'] === true - ) { - // since only one may be set, set both variations, prefer camelCase and boolean for coding - // this will also maintain --json backwards compatibility for the all lower case scenario - defaultFields['generatepassword'] = 'true'; - defaultFields['generatePassword'] = true; - } - // for the false case - if ( - defaultFields['generatepassword'] === 'false' || - defaultFields['generatePassword'] === 'false' || - defaultFields['generatePassword'] === false - ) { - // since only one may be set, set both variations, prefer camelCase and boolean for coding - // this will also maintain --json backwards compatibility for the all lower case scenario - defaultFields['generatepassword'] = 'false'; - defaultFields['generatePassword'] = false; - } - return defaultFields; } @@ -258,3 +256,9 @@ export class UserCreateCommand extends SfdxCommand { } export default UserCreateCommand; + +interface UserCreateOutput { + orgId: string; + permissionSetAssignments: string[]; + fields: Record; +} diff --git a/test/commands/user/create.test.ts b/test/commands/user/create.test.ts index 9e79357f..c3c6cc31 100644 --- a/test/commands/user/create.test.ts +++ b/test/commands/user/create.test.ts @@ -8,7 +8,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { $$, expect, test } from '@salesforce/command/lib/test'; -import { Aliases, AuthInfo, Connection, DefaultUserFields, fs, Logger, Org, User } from '@salesforce/core'; +import { Aliases, AuthInfo, Connection, DefaultUserFields, fs, Logger, Org, User, UserFields } from '@salesforce/core'; import { stubMethod } from '@salesforce/ts-sinon'; import { IConfig } from '@oclif/config'; import UserCreateCommand from '../../../src/commands/force/user/create'; @@ -48,7 +48,6 @@ describe('force:user:create', () => { lastName: 'User', timeZoneSidKey: 'America/Los_Angeles', }); - expect(res).to.deep.equal({ alias: 'testAlias', email: 'defaultusername@test.com', @@ -67,7 +66,7 @@ describe('force:user:create', () => { async function prepareStubs(throws: { license?: boolean; duplicate?: boolean } = {}, readsFile?) { stubMethod($$.SANDBOX, Org.prototype, 'getConnection').callsFake(() => Connection.prototype); stubMethod($$.SANDBOX, DefaultUserFields, 'create').resolves({ - getFields: () => { + getFields: (): UserFields => { return { id: '0052D0000043PawWWR', username: '1605130295132_test-j6asqt5qoprs@example.com', @@ -84,6 +83,8 @@ describe('force:user:create', () => { }); stubMethod($$.SANDBOX, Aliases, 'fetch').resolves('testAlias'); stubMethod($$.SANDBOX, User, 'create').callsFake(() => User.prototype); + + stubMethod($$.SANDBOX, User.prototype, 'assignPermissionSets').resolves(); stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns(username); stubMethod($$.SANDBOX, Org.prototype, 'getOrgId').returns('abc123'); authInfoStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); @@ -108,7 +109,7 @@ describe('force:user:create', () => { test .do(async () => { stubMethod($$.SANDBOX, User.prototype, 'assignPassword').resolves(); - await prepareStubs({}, { profileName: 'profileFromFile', permsets: ['perm1', 'perm2'] }); + await prepareStubs({}, { profilename: 'profileFromFile', permsets: ['perm1', 'perm2'] }); }) .stdout() .command([ @@ -120,25 +121,26 @@ describe('force:user:create', () => { 'devhub@test.com', "permsets='permCLI, permCLI2'", 'generatepassword=true', - 'profileName=profileFromArgs', + 'profilename=profileFromArgs', ]) - .it('will handle a merge multiple permsets and profileNames from args and file (permsets from args)', (ctx) => { + .it('will handle a merge multiple permsets and profilenames from args and file (permsets from args)', (ctx) => { const expected = { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - localeSidKey: 'en_US', orgId: 'abc123', - generatepassword: 'true', - generatePassword: true, - permsets: "'permCLI, permCLI2'", - profileId: '12345678', - profileName: 'profileFromArgs', - timeZoneSidKey: 'America/Los_Angeles', - username: '1605130295132_test-j6asqt5qoprs@example.com', + permissionSetAssignments: ['permCLI', 'permCLI2'], + fields: { + alias: 'testAlias', + email: 'defaultusername@test.com', + emailencodingkey: 'UTF-8', + id: '0052D0000043PawWWR', + languagelocalekey: 'en_US', + lastname: 'User', + localesidkey: 'en_US', + generatepassword: true, + profileid: '12345678', + profilename: 'profileFromArgs', + timezonesidkey: 'America/Los_Angeles', + username: '1605130295132_test-j6asqt5qoprs@example.com', + }, }; const result = JSON.parse(ctx.stdout).result; expect(result).to.deep.equal(expected); @@ -159,24 +161,26 @@ describe('force:user:create', () => { 'devhub@test.com', '--definitionfile', 'tempfile.json', - 'profileName=profileFromArgs', + 'profilename=profileFromArgs', 'username=user@cliArgs.com', ]) - .it('will handle a merge multiple permsets and profileNames from args and file (permsets from file)', (ctx) => { + .it('will handle a merge multiple permsets and profilenames from args and file (permsets from file)', (ctx) => { const expected = { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - localeSidKey: 'en_US', orgId: 'abc123', - permsets: ['perm1', 'perm2'], - profileId: '12345678', - profileName: 'profileFromArgs', - timeZoneSidKey: 'America/Los_Angeles', - username: 'user@cliArgs.com', + permissionSetAssignments: ['perm1', 'perm2'], + fields: { + alias: 'testAlias', + email: 'defaultusername@test.com', + emailencodingkey: 'UTF-8', + id: '0052D0000043PawWWR', + languagelocalekey: 'en_US', + lastname: 'User', + localesidkey: 'en_US', + profileid: '12345678', + profilename: 'profileFromArgs', + timezonesidkey: 'America/Los_Angeles', + username: 'user@cliArgs.com', + }, }; const result = JSON.parse(ctx.stdout).result; expect(result).to.deep.equal(expected); @@ -198,17 +202,20 @@ describe('force:user:create', () => { ]) .it('default create creates user exactly from DefaultUserFields', (ctx) => { const expected = { - alias: 'testAlias', - email: username, - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - localeSidKey: 'en_US', orgId: 'abc123', - profileId: '00e2D000000bNexWWR', - timeZoneSidKey: 'America/Los_Angeles', - username: '1605130295132_test-j6asqt5qoprs@example.com', + permissionSetAssignments: [], + fields: { + alias: 'testAlias', + email: username, + emailencodingkey: 'UTF-8', + id: '0052D0000043PawWWR', + languagelocalekey: 'en_US', + lastname: 'User', + localesidkey: 'en_US', + profileid: '00e2D000000bNexWWR', + timezonesidkey: 'America/Los_Angeles', + username: '1605130295132_test-j6asqt5qoprs@example.com', + }, }; const result = JSON.parse(ctx.stdout).result; expect(result).to.deep.equal(expected); @@ -235,20 +242,21 @@ describe('force:user:create', () => { // we set generatepassword=false in the varargs, in the definitionfile we have generatepassword=true, so we SHOULD NOT generate a password .it('will merge fields from the cli args, and the definitionfile correctly, preferring cli args', (ctx) => { const expected = { - alias: 'testAlias', - email: 'me@my.org', - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - localeSidKey: 'en_US', orgId: 'abc123', - permsets: ['test1', 'test2'], - profileId: '00e2D000000bNexWWR', - generatePassword: false, - generatepassword: 'false', - timeZoneSidKey: 'America/Los_Angeles', - username: '1605130295132_test-j6asqt5qoprs@example.com', + permissionSetAssignments: ['test1', 'test2'], + fields: { + alias: 'testAlias', + email: 'me@my.org', + emailencodingkey: 'UTF-8', + id: '0052D0000043PawWWR', + languagelocalekey: 'en_US', + lastname: 'User', + localesidkey: 'en_US', + profileid: '00e2D000000bNexWWR', + generatepassword: false, + timezonesidkey: 'America/Los_Angeles', + username: '1605130295132_test-j6asqt5qoprs@example.com', + }, }; const result = JSON.parse(ctx.stdout).result; expect(result).to.deep.equal(expected); @@ -257,7 +265,7 @@ describe('force:user:create', () => { test .do(async () => { - await prepareStubs({}, { generatepassword: true, profileName: 'System Administrator' }); + await prepareStubs({}, { generatepassword: true, profilename: 'System Administrator' }); }) .stdout() .command([ @@ -271,29 +279,31 @@ describe('force:user:create', () => { 'devhub@test.com', 'email=me@my.org', 'generatepassword=false', - "profileName='Chatter Free User'", + "profilename='Chatter Free User'", ]) // we set generatepassword=false in the varargs, in the definitionfile we have generatepassword=true, so we SHOULD NOT generate a password - // we should override the profileName with 'Chatter Free User' + // we should override the profilename with 'Chatter Free User' .it( 'will merge fields from the cli args, and the definitionfile correctly, preferring cli args, cli args > file > default', (ctx) => { const expected = { - alias: 'testAlias', - email: 'me@my.org', - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - localeSidKey: 'en_US', - generatePassword: false, - generatepassword: 'false', - profileName: "'Chatter Free User'", orgId: 'abc123', - // note the new profileId 12345678 -> Chatter Free User from var args - profileId: '12345678', - timeZoneSidKey: 'America/Los_Angeles', - username: '1605130295132_test-j6asqt5qoprs@example.com', + permissionSetAssignments: [], + fields: { + alias: 'testAlias', + email: 'me@my.org', + emailencodingkey: 'UTF-8', + id: '0052D0000043PawWWR', + languagelocalekey: 'en_US', + lastname: 'User', + localesidkey: 'en_US', + generatepassword: false, + profilename: "'Chatter Free User'", + // note the new profileid 12345678 -> Chatter Free User from var args + profileid: '12345678', + timezonesidkey: 'America/Los_Angeles', + username: '1605130295132_test-j6asqt5qoprs@example.com', + }, }; const result = JSON.parse(ctx.stdout).result; expect(result).to.deep.equal(expected);