diff --git a/.eslintrc.js b/.eslintrc.js index 83ad149c..989e67e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,5 +5,5 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ module.exports = { - extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license'], + extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/migration'], }; diff --git a/command-snapshot.json b/command-snapshot.json index cb4c9380..f94fffba 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,56 +1,74 @@ [ { - "command": "force:user:create", + "command": "force:user:password:generate", "plugin": "@salesforce/plugin-user", "flags": [ - "apiversion", - "definitionfile", + "api-version", + "complexity", "json", + "length", "loglevel", - "setalias", - "setuniqueusername", - "targetdevhubusername", - "targetusername" + "on-behalf-of", + "target-dev-hub", + "target-org" ], "alias": [] }, { - "command": "force:user:display", + "command": "force:user:permset:assign", "plugin": "@salesforce/plugin-user", - "flags": ["apiversion", "json", "loglevel", "targetdevhubusername", "targetusername"], + "flags": ["api-version", "json", "loglevel", "on-behalf-of", "perm-set-name", "target-org"], "alias": [] }, { - "command": "force:user:list", + "command": "force:user:permsetlicense:assign", "plugin": "@salesforce/plugin-user", - "flags": ["apiversion", "json", "loglevel", "targetdevhubusername", "targetusername"], + "flags": ["api-version", "json", "loglevel", "name", "on-behalf-of", "target-org"], "alias": [] }, { - "command": "force:user:password:generate", + "command": "user:create", "plugin": "@salesforce/plugin-user", "flags": [ - "apiversion", - "complexity", + "api-version", + "definition-file", "json", - "length", "loglevel", - "onbehalfof", - "targetdevhubusername", - "targetusername" + "set-alias", + "set-unique-username", + "target-dev-hub", + "target-org" ], - "alias": [] + "alias": ["force:user:create", "org:create:user"] }, { - "command": "force:user:permset:assign", + "command": "user:display", "plugin": "@salesforce/plugin-user", - "flags": ["apiversion", "json", "loglevel", "onbehalfof", "permsetname", "targetusername"], - "alias": [] + "flags": ["api-version", "json", "loglevel", "target-dev-hub", "target-org", "verbose"], + "alias": ["force:user:display", "org:display:user"] }, { - "command": "force:user:permsetlicense:assign", + "command": "user:list", "plugin": "@salesforce/plugin-user", - "flags": ["apiversion", "json", "loglevel", "name", "onbehalfof", "targetusername"], - "alias": [] + "flags": ["api-version", "json", "loglevel", "target-dev-hub", "target-org"], + "alias": ["force:user:list", "org:list:users"] + }, + { + "command": "user:password:generate", + "plugin": "@salesforce/plugin-user", + "flags": ["api-version", "complexity", "json", "length", "on-behalf-of", "target-org"], + "alias": ["org:generate:password"] + }, + { + "command": "user:permset:assign", + "plugin": "@salesforce/plugin-user", + "flags": ["api-version", "json", "on-behalf-of", "perm-set-name", "target-org"], + "alias": ["org:assign:permset"] + }, + { + "command": "user:permsetlicense:assign", + "plugin": "@salesforce/plugin-user", + "flags": ["api-version", "json", "name", "on-behalf-of", "target-org"], + "alias": ["org:assign:permsetlicense"] } ] diff --git a/messages/create.json b/messages/create.json deleted file mode 100644 index 5221f579..00000000 --- a/messages/create.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "description": "create a user for a scratch org\nCreate a user for a scratch org, optionally setting an alias for use by the CLI, assigning permission sets (e.g., permsets=ps1,ps2), generating a password (e.g., generatepassword=true), and setting User sObject fields.", - "examples": [ - "$ sfdx force:user:create", - "$ sfdx force:user:create -a testuser1 -f config/project-user-def.json profileName='Chatter Free User'", - "$ sfdx force:user:create username=testuser1@my.org email=me@my.org permsets=DreamHouse", - "$ sfdx force:user:create -f config/project-user-def.json email=me@my.org generatepassword=true" - ], - "flags": { - "alias": "set an alias for the created username to reference within the CLI", - "definitionfile": "file path to a user definition", - "setuniqueusername": "force the username, if specified in the definition file or at the command line, to be unique by appending the org ID" - }, - "licenseLimitExceeded": "There are no available user licenses for the user profile \"%s\".", - "duplicateUsername": "The username \"%s\" already exists in this or another Salesforce org. Usernames must be unique across all Salesforce orgs.", - "success": "Successfully created user \"%s\" with ID %s for org %s.%sYou can see more details about this user by running \"sfdx force:user:display -u %s\"." -} diff --git a/messages/create.md b/messages/create.md new file mode 100644 index 00000000..e7d4752a --- /dev/null +++ b/messages/create.md @@ -0,0 +1,39 @@ +# summary + +create a user for a scratch org + +# description + +Create a user for a scratch org, optionally setting an alias for use by the CLI, assigning permission sets (e.g., permsets=ps1,ps2), generating a password (e.g., generatepassword=true), and setting User sObject fields., + +# examples + +- <%= config.bin %> <%= command.id %> +- <%= config.bin %> <%= command.id %> -a testuser1 -f config/project-user-def.json profileName='Chatter Free User' +- <%= config.bin %> <%= command.id %> username=testuser1@my.org email=me@my.org permsets=DreamHouse +- <%= config.bin %> <%= command.id %> -f config/project-user-def.json email=me@my.org generatepassword=true + +# flags.alias.summary + +set an alias for the created username to reference within the CLI, + +# flags.definitionfile.summary + +file path to a user definition, + +# flags.setuniqueusername.summary + +force the username, if specified in the definition file or at the command line, to be unique by appending the org ID + +# licenseLimitExceeded + +There are no available user licenses for the user profile "%s". + +# duplicateUsername + +The username "%s" already exists in this or another Salesforce org. Usernames must be unique across all Salesforce orgs. + +# success + +Successfully created user "%s" with ID %s for org %s.%s +You can see more details about this user by running "<%= config.bin %> user:display -u %s". diff --git a/messages/display.json b/messages/display.json deleted file mode 100644 index 231696fc..00000000 --- a/messages/display.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "description": "displays information about a user of a scratch org\nOutput includes the profile name, org ID, access token, instance URL, login URL, and alias if applicable.", - "examples": ["$ sfdx force:user:display", "$ sfdx force:user:display -u me@my.org --json"], - "accessTokenError": "This command doesn't accept an access token for a username.", - "accessTokenAction": "Specify a username or an alias.", - "securityWarning": "This command will expose sensitive information that allows for subsequent activity using your current authenticated session.\nSharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege.\nFor 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" -} diff --git a/messages/display.md b/messages/display.md new file mode 100644 index 00000000..88216e2f --- /dev/null +++ b/messages/display.md @@ -0,0 +1,18 @@ +# summary + +displays information about a user of a scratch org + +# description + +Output includes the profile name, org ID, access token, instance URL, login URL, and alias if applicable. + +# examples + +- <%= config.bin %> <%= command.id %> +- <%= config.bin %> <%= command.id %> -u me@my.org --json + +# securityWarning + +This command will expose sensitive information that allows for subsequent activity using your current authenticated session. +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 diff --git a/messages/list.json b/messages/list.json deleted file mode 100644 index 8863e48c..00000000 --- a/messages/list.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "description": "list all authenticated users of an org\nThe original scratch org admin is marked with \"(A)\"", - "examples": [ - "$ sfdx force:user:list", - "$ sfdx force:user:list -u me@my.org --json", - "$ sfdx force:user:list --json > tmp/MyUserList.json" - ] -} diff --git a/messages/list.md b/messages/list.md new file mode 100644 index 00000000..32771ad6 --- /dev/null +++ b/messages/list.md @@ -0,0 +1,17 @@ +# summary + +list all authenticated users of an org + +# description + +The original scratch org admin is marked with "(A)" + +# examples + +- <%= config.bin %> <%= command.id %> +- <%= config.bin %> <%= command.id %> -u me@my.org --json +- <%= config.bin %> <%= command.id %> --json > tmp/MyUserList.json + +# flags.target-hub.summary + +Username or alias of the Dev Hub org. diff --git a/messages/password.generate.json b/messages/password.generate.json deleted file mode 100644 index 9cb3f6f4..00000000 --- a/messages/password.generate.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "description": "generate a password for scratch org users\nGenerates and sets a random password for one or more scratch org users. Targets the usernames listed with the --onbehalfof parameter or the --targetusername parameter. Defaults to the defaultusername.\n\nIf you haven’t set a default Dev Hub, or if your scratch org isn’t associated with your default Dev Hub, --targetdevhubusername is required.\n\nTo change the password strength, set the --complexity parameter to a value between 0 and 5. Each value specifies the types of characters used in the generated password: \n\n0 - lower case letters only\n1 - lower case letters and numbers only\n2 - lower case letters and symbols only\n3 - lower and upper case letters and numbers only\n4 - lower and upper case letters and symbols only\n5 - lower and upper case letters and numbers and symbols only \n\nTo see a password that was previously generated, run \"sfdx force:user:display\".", - "examples": [ - "$ sfdx force:user:password:generate", - "$ sfdx force:user:password:generate -l 12", - "$ sfdx force:user:password:generate -c 3", - "$ sfdx force:user:password:generate -u me@my.org --json", - "$ sfdx force:user:password:generate -o \"user1@my.org,user2@my.org,user3@my.org\"" - ], - "flags": { - "onBehalfOf": "comma-separated list of usernames or aliases to assign the password to", - "length": "number of characters in the generated password; valid values are between 8 and 1000", - "complexity": "level of password complexity or strength; the higher the value, the stronger the password" - }, - "noSelfSetErrorV50": "Create a scratch org with the enableSetPasswordInApi org security setting set to TRUE and try again.", - "noSelfSetError": "Starting in Spring '21, EnableSetPasswordInApi is a feature in your scratch org definition file and not a setting. This change is a result of the field Settings.securitySettings.passwordPolicies.enableSetPasswordInApi being deprecated in version 51.0 of the Metadata API.", - "noSelfSetErrorActions": [ - "Update your scratch org definition file and remove enableSetPasswordInApi from the \"securitySettings\" setting. Then add EnableSetPasswordInApi as a feature. For example:", - "", - "\"features\": [\"EnableSetPasswordInApi\"]", - "", - "Then try creating the scratch org again." - ], - "scratchFeaturesUrl": "see https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_def_file_config_values.htm", - "success": "Successfully set the password \"%s\" for user %s.", - "successMultiple": "Successfully set passwords:%s", - "viewWithCommand": "You can see the password again by running \"sfdx force:user:display -u %s\"." -} diff --git a/messages/password.generate.md b/messages/password.generate.md new file mode 100644 index 00000000..1c51b5f8 --- /dev/null +++ b/messages/password.generate.md @@ -0,0 +1,78 @@ +# summary + +generate a password for scratch org users + +# description + +Generates and sets a random password for one or more scratch org users. Targets the usernames listed with the --onbehalfof parameter or the --targetusername parameter. Defaults to the defaultusername. + +If you haven’t set a default Dev Hub, or if your scratch org isn’t associated with your default Dev Hub, --targetdevhubusername is required. + +To change the password strength, set the --complexity parameter to a value between 0 and 5. Each value specifies the types of characters used in the generated password: + +0 - lower case letters only +1 - lower case letters and numbers only +2 - lower case letters and symbols only +3 - lower and upper case letters and numbers only +4 - lower and upper case letters and symbols only +5 - lower and upper case letters and numbers and symbols only + +To see a password that was previously generated, run "<%= config.bin %> user:display". + +# examples + +- <%= config.bin %> <%= command.id %> +- <%= config.bin %> <%= command.id %> -l 12 +- <%= config.bin %> <%= command.id %> -c 3 +- <%= config.bin %> <%= command.id %> -u me@my.org --json +- <%= config.bin %> <%= command.id %> -o "user1@my.org,user2@my.org,user3@my.org" + +# flags.onBehalfOf + +comma-separated list of usernames or aliases to assign the password to + +# flags.length + +number of characters in the generated password; valid values are between 8 and 1000 + +# flags.complexity + +level of password complexity or strength; the higher the value, the stronger the password + +# noSelfSetErrorV50 + +Create a scratch org with the enableSetPasswordInApi org security setting set to TRUE and try again. + +# noSelfSetError + +Starting in Spring '21, EnableSetPasswordInApi is a feature in your scratch org definition file and not a setting. This change is a result of the field Settings.securitySettings.passwordPolicies.enableSetPasswordInApi being deprecated in version 51.0 of the Metadata API. + +# noSelfSetError.actions + +- Update your scratch org definition file and remove enableSetPasswordInApi from the "securitySettings" setting. Then add EnableSetPasswordInApi as a feature. For example: +- "features": ["EnableSetPasswordInApi"] +- Then try creating the scratch org again. + +# scratchFeaturesUrl + +see https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_def_file_config_values.htm + +# success + +Successfully set the password "%s" for user %s. + +# successMultiple + +Successfully set passwords:%s + +# viewWithCommand + +You can see the password again by running "sfdx user:display -u %s". + +# flags.target-org.summary + +Scratch org alias or login user. + +# onBehalfOfMultipleError + +Found a comma-separated list of usernames or aliases for the --onbehalfof parameter. Either specify one per flag or separate by a space. diff --git a/messages/permset.assign.json b/messages/permset.assign.json deleted file mode 100644 index cf2e8a2c..00000000 --- a/messages/permset.assign.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "description": "assign a permission set to one or more users of an org\nTo specify an alias for the -u or -o parameter, use the username alias you set with the \"alias:set\" CLI command, not the User.Alias value of the org user.", - "examples": [ - "$ sfdx force:user:permset:assign -n \"DreamHouse, LargeDreamHouse\"", - "$ sfdx force:user:permset:assign -n DreamHouse -u me@my.org", - "$ sfdx force:user:permset:assign -n DreamHouse -o \"user1@my.org,user2,user3\"" - ], - "flags": { - "onBehalfOf": "comma-separated list of usernames or aliases to assign the permission set to", - "permsetName": "comma-separated list of permission sets to assign" - } -} diff --git a/messages/permset.assign.md b/messages/permset.assign.md new file mode 100644 index 00000000..adaf849a --- /dev/null +++ b/messages/permset.assign.md @@ -0,0 +1,25 @@ +# summary + +assign a permission set to one or more users of an org + +# description + +To specify an alias for the -u or -o parameter, use the username alias you set with the "alias:set" CLI command, not the User.Alias value of the org user. + +# examples + +- <%= config.bin %> <%= command.id %> -n "DreamHouse, LargeDreamHouse", +- <%= config.bin %> <%= command.id %> -n DreamHouse -u me@my.org, +- <%= config.bin %> <%= command.id %> -n DreamHouse -o "user1@my.org,user2,user3" + +# flags.onBehalfOf + +comma-separated list of usernames or aliases to assign the permission set to + +# flags.permsetName + +comma-separated list of permission sets to assign + +# flags.target-org.summary + +Scratch org alias or login user. diff --git a/messages/permsetlicense.assign.json b/messages/permsetlicense.assign.json deleted file mode 100644 index 1dfd2bd8..00000000 --- a/messages/permsetlicense.assign.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "assign a permission set license to one or more users of an org", - "examples": [ - "$ sfdx force:user:permsetlicense:assign -n DreamHouse", - "$ sfdx force:user:permsetlicense:assign -n DreamHouse -u me@my.org", - "$ sfdx force:user:permsetlicense:assign -n DreamHouse -o \"user1@my.org,user2,user3\"" - ], - "flags": { - "onBehalfOf": "comma-separated list of usernames or aliases to assign the permission set license to", - "name": "the name of the permission set license to assign" - }, - "duplicateValue": "The user \"%s\" already has the Permission Set License \"%s\"" -} diff --git a/messages/permsetlicense.assign.md b/messages/permsetlicense.assign.md new file mode 100644 index 00000000..95069453 --- /dev/null +++ b/messages/permsetlicense.assign.md @@ -0,0 +1,29 @@ +# summary + +assign a permission set license to one or more users of an org + +# description + +assign a permission set license to one or more users of an org + +# examples + +<%= config.bin %> <%= command.id %> -n DreamHouse, +<%= config.bin %> <%= command.id %> -n DreamHouse -u me@my.org, +<%= config.bin %> <%= command.id %> -n DreamHouse -o "user1@my.org,user2,user3" + +# flags.onBehalfOf + +comma-separated list of usernames or aliases to assign the permission set license to + +# flags.name + +the name of the permission set license to assign + +# duplicateValue + +The user "%s" already has the Permission Set License "%s" + +# flags.target-org.summary + +Scratch org alias or login user. diff --git a/package.json b/package.json index 1099432d..a94f399d 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { "name": "@salesforce/plugin-user", "description": "Commands to interact with Users and Permission Sets", - "version": "2.1.23", + "version": "2.1.22", "author": "Salesforce", "main": "lib/index.js", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { "@oclif/core": "^1.22.0", - "@salesforce/command": "^5.2.28", - "@salesforce/core": "^3.32.6", - "@salesforce/kit": "^1.7.1", + "@salesforce/core": "^3.32.11", + "@salesforce/kit": "^1.8.0", + "@salesforce/sf-plugins-core": "^1.21.3", + "@salesforce/ts-types": "^1.7.1", "tslib": "^2" }, "devDependencies": { @@ -17,11 +18,11 @@ "@salesforce/cli-plugins-testkit": "^3.2.6", "@salesforce/dev-config": "^3.0.0", "@salesforce/dev-scripts": "^3.1.0", - "@salesforce/plugin-command-reference": "^1.5.6", + "@salesforce/plugin-command-reference": "^2.2.8", "@salesforce/prettier-config": "^0.0.2", "@salesforce/ts-sinon": "1.4.2", - "@swc/core": "^1.3.23", - "@typescript-eslint/eslint-plugin": "^5.44.0", + "@swc/core": "^1.3.22", + "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.7", "chai-each": "^0.0.1", @@ -33,6 +34,7 @@ "eslint-plugin-header": "^3.0.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-jsdoc": "^39.6.4", + "eslint-plugin-sf-plugin": "^1.2.1", "husky": "^7.0.4", "mocha": "^9.1.3", "nyc": "^15.1.0", @@ -80,12 +82,29 @@ "permset": { "description": "Use to interact with permission sets assigned to a user" }, + "permsetlicense": { + "description": "Use to interact with permission sets assigned to a user" + }, "password": { "description": "Used to generate and set passwords for users" } } } } + }, + "user": { + "description": "commands that perform user-related admin tasks", + "subtopics": { + "permset": { + "description": "Use to interact with permission sets assigned to a user" + }, + "permsetlicense": { + "description": "Use to interact with permission sets assigned to a user" + }, + "password": { + "description": "Used to generate and set passwords for users" + } + } } } }, @@ -100,17 +119,18 @@ "format": "sf-format", "lint": "sf-lint", "postpack": "shx rm -f oclif.manifest.json", - "posttest": "yarn lint && yarn test:deprecation-policy && yarn test:command-reference", + "posttest": "yarn lint && yarn test:deprecation-policy && yarn test:command-reference && yarn test:json-schema", "prepack": "sf-prepack", "prepare": "sf-install", "pretest": "sf-compile-test", "test": "sf-test", "test:command-reference": "./bin/dev commandreference:generate --erroronwarnings", "test:deprecation-policy": "./bin/dev snapshot:compare", + "test:json-schema": "./bin/dev schema:compare", "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", "version": "oclif readme" }, "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/schemas/force-user-password-generate.json b/schemas/force-user-password-generate.json new file mode 100644 index 00000000..9220e462 --- /dev/null +++ b/schemas/force-user-password-generate.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GenerateResult", + "definitions": { + "GenerateResult": { + "anyOf": [ + { + "$ref": "#/definitions/PasswordData" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/PasswordData" + } + } + ] + }, + "PasswordData": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["password"], + "additionalProperties": false + } + } +} diff --git a/schemas/force-user-permset-assign.json b/schemas/force-user-permset-assign.json new file mode 100644 index 00000000..eaa2b7af --- /dev/null +++ b/schemas/force-user-permset-assign.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PermsetAssignResult", + "definitions": { + "PermsetAssignResult": { + "type": "object", + "properties": { + "successes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["name", "value"], + "additionalProperties": false + } + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["name", "message"], + "additionalProperties": false + } + } + }, + "required": ["successes", "failures"], + "additionalProperties": false + } + } +} diff --git a/schemas/force-user-permsetlicense-assign.json b/schemas/force-user-permsetlicense-assign.json new file mode 100644 index 00000000..3a444aeb --- /dev/null +++ b/schemas/force-user-permsetlicense-assign.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PSLResult", + "definitions": { + "PSLResult": { + "type": "object", + "properties": { + "successes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["name", "value"], + "additionalProperties": false + } + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["name", "message"], + "additionalProperties": false + } + } + }, + "required": ["successes", "failures"], + "additionalProperties": false + } + } +} diff --git a/schemas/user-create.json b/schemas/user-create.json new file mode 100644 index 00000000..c39e0b1f --- /dev/null +++ b/schemas/user-create.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/UserCreateOutput", + "definitions": { + "UserCreateOutput": { + "type": "object", + "properties": { + "orgId": { + "type": "string" + }, + "permissionSetAssignments": { + "type": "array", + "items": { + "type": "string" + } + }, + "fields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["orgId", "permissionSetAssignments", "fields"], + "additionalProperties": false + } + } +} diff --git a/schemas/user-display.json b/schemas/user-display.json new file mode 100644 index 00000000..331a14c9 --- /dev/null +++ b/schemas/user-display.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/UserDisplayResult", + "definitions": { + "UserDisplayResult": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "profileName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "orgId": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "instanceUrl": { + "type": "string" + }, + "loginUrl": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["username", "profileName", "id", "orgId"], + "additionalProperties": false + } + } +} diff --git a/schemas/user-list.json b/schemas/user-list.json new file mode 100644 index 00000000..85adecb6 --- /dev/null +++ b/schemas/user-list.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/UserList", + "definitions": { + "UserList": { + "type": "array", + "items": { + "$ref": "#/definitions/AuthList" + } + }, + "AuthList": { + "type": "object", + "properties": { + "defaultMarker": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "username": { + "type": "string" + }, + "profileName": { + "type": "string" + }, + "orgId": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "instanceUrl": { + "type": "string" + }, + "loginUrl": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/schemas/user-password-generate.json b/schemas/user-password-generate.json new file mode 100644 index 00000000..9220e462 --- /dev/null +++ b/schemas/user-password-generate.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GenerateResult", + "definitions": { + "GenerateResult": { + "anyOf": [ + { + "$ref": "#/definitions/PasswordData" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/PasswordData" + } + } + ] + }, + "PasswordData": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["password"], + "additionalProperties": false + } + } +} diff --git a/schemas/user-permset-assign.json b/schemas/user-permset-assign.json new file mode 100644 index 00000000..eaa2b7af --- /dev/null +++ b/schemas/user-permset-assign.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PermsetAssignResult", + "definitions": { + "PermsetAssignResult": { + "type": "object", + "properties": { + "successes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["name", "value"], + "additionalProperties": false + } + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["name", "message"], + "additionalProperties": false + } + } + }, + "required": ["successes", "failures"], + "additionalProperties": false + } + } +} diff --git a/schemas/user-permsetlicense-assign.json b/schemas/user-permsetlicense-assign.json new file mode 100644 index 00000000..3a444aeb --- /dev/null +++ b/schemas/user-permsetlicense-assign.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PSLResult", + "definitions": { + "PSLResult": { + "type": "object", + "properties": { + "successes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["name", "value"], + "additionalProperties": false + } + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["name", "message"], + "additionalProperties": false + } + } + }, + "required": ["successes", "failures"], + "additionalProperties": false + } + } +} diff --git a/src/baseCommands/user/password/generate.ts b/src/baseCommands/user/password/generate.ts new file mode 100644 index 00000000..54ba1a3a --- /dev/null +++ b/src/baseCommands/user/password/generate.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2020, 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 * as os from 'os'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { AuthInfo, Connection, StateAggregator, Messages, Org, SfError, User } from '@salesforce/core'; +import { PasswordConditions } from '@salesforce/core/lib/org/user'; +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-user', 'password.generate'); + +export type PasswordData = { + username?: string; + password: string; +}; + +export type GenerateResult = PasswordData | PasswordData[]; + +export abstract class UserPasswordGenerateBaseCommand extends SfCommand { + protected usernames: string[]; + protected passwordData: PasswordData[] = []; + protected connection: Connection; + protected org: Org; + protected length: number; + protected complexity: number; + + public async generate(): Promise { + // const { flags } = await this.parse(UserPasswordGenerateCommand); + // this.usernames = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + + const passwordCondition: PasswordConditions = { + length: this.length, + complexity: this.complexity, + }; + + // sequentially to avoid auth file collisions until configFile if safer + /* eslint-disable no-await-in-loop */ + for (const aliasOrUsername of this.usernames) { + try { + // Convert any aliases to usernames + // fetch will return undefined if there's no Alias for that name + const username = (await StateAggregator.getInstance()).aliases.resolveUsername(aliasOrUsername); + + const authInfo: AuthInfo = await AuthInfo.create({ username }); + const connection: Connection = await Connection.create({ authInfo }); + const org = await Org.create({ connection }); + const user: User = await User.create({ org }); + const password = User.generatePasswordUtf8(passwordCondition); + // we only need the Id, so instead of User.retrieve we'll just query + // this avoids permission issues if ProfileId is restricted for the user querying for it + const result: { Id: string } = await connection.singleRecordQuery( + `SELECT Id FROM User WHERE Username='${username}'` + ); + + // userId is used by `assignPassword` so we need to set it here + authInfo.getFields().userId = result.Id; + await user.assignPassword(authInfo, password); + + password.value((pass) => { + this.passwordData.push({ username, password: pass.toString('utf-8') }); + authInfo.update({ password: pass.toString('utf-8') }); + }); + + await authInfo.save(); + } catch (e) { + const err = e as SfError; + if ( + err.message.includes('Cannot set password for self') || + err.message.includes('The requested Resource does not exist') + ) { + // we don't have access to the apiVersion from what happened in the try, so until v51 is r2, we have to check versions the hard way + const authInfo: AuthInfo = await AuthInfo.create({ username: aliasOrUsername }); + const connection: Connection = await Connection.create({ authInfo }); + const org = await Org.create({ connection }); + if (parseInt(await org.retrieveMaxApiVersion(), 10) >= 51) { + throw messages.createError('noSelfSetError'); + } + throw new SfError(messages.getMessage('noSelfSetErrorV50'), 'noSelfSetErrorError'); + } + throw SfError.wrap(err); + } + } + /* eslint-enable no-await-in-loop */ + + this.print(); + + return this.passwordData.length === 1 ? this.passwordData[0] : this.passwordData; + } + + private print(): void { + if (this.passwordData) { + const successMsg = messages.getMessage('success', [this.passwordData[0].password, this.passwordData[0].username]); + const viewMsg = messages.getMessage('viewWithCommand', [this.passwordData[0].username]); + this.log(`${successMsg}${os.EOL}${viewMsg}`); + } else { + this.log(messages.getMessage('successMultiple', [os.EOL])); + const columnData = { + username: { header: 'USERNAME' }, + password: { header: 'PASSWORD' }, + }; + this.table(this.passwordData, columnData); + } + } +} diff --git a/src/baseCommands/user/permset/assign.ts b/src/baseCommands/user/permset/assign.ts new file mode 100644 index 00000000..c4853824 --- /dev/null +++ b/src/baseCommands/user/permset/assign.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020, 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 { Connection, Messages, Org, SfError, StateAggregator, User } from '@salesforce/core'; +import { SfCommand } from '@salesforce/sf-plugins-core'; + +Messages.importMessagesDirectory(__dirname); + +type SuccessMsg = { + name: string; + value: string; +}; + +type FailureMsg = { + name: string; + message: string; +}; + +export type PermsetAssignResult = { + successes: SuccessMsg[]; + failures: FailureMsg[]; +}; + +export abstract class UserPermSetAssignBaseCommand extends SfCommand { + protected connection: Connection; + protected org: Org; + protected aliasOrUsernames: string[] = []; + protected permSetNames: string[] = []; + protected readonly successes: SuccessMsg[] = []; + protected readonly failures: FailureMsg[] = []; + + public async assign(): Promise { + try { + // sequentially to avoid auth file collisions until configFile if safer + /* eslint-disable no-await-in-loop */ + for (const aliasOrUsername of this.aliasOrUsernames) { + // Attempt to convert any aliases to usernames. Not found alias will be **assumed** to be a username + const username = (await StateAggregator.getInstance()).aliases.resolveUsername(aliasOrUsername); + const user: User = await User.create({ org: this.org }); + // get userId of whomever the permset will be assigned to via query to avoid AuthInfo if remote user + const queryResult = await this.connection.singleRecordQuery<{ Id: string }>( + `SELECT Id FROM User WHERE Username='${username}'` + ); + // this is hard to parallelize because core returns void instead of some result object we can handle. Promise.allSettled might work + for (const permSetName of this.permSetNames) { + try { + await user.assignPermissionSets(queryResult.Id, [permSetName]); + this.successes.push({ + name: aliasOrUsername, + value: permSetName, + }); + } catch (e) { + const err = e as SfError; + this.failures.push({ + name: aliasOrUsername, + message: err.message, + }); + } + } + } + /* eslint-enable no-await-in-loop */ + } catch (e) { + if (e instanceof Error || typeof e === 'string') { + throw SfError.wrap(e); + } + throw e; + } + + this.print(); + this.setExitCode(); + + return { + successes: this.successes, + failures: this.failures, + }; + } + + private print(): void { + if (this.failures.length > 0 && this.successes.length > 0) { + this.styledHeader('Partial Success'); + this.styledHeader('Permsets Assigned'); + this.table(this.successes, { name: { header: 'Username' }, value: { header: 'Permission Set Assignment' } }); + this.log(''); + this.styledHeader('Failures'); + this.table(this.failures, { name: { header: 'Username' }, message: { header: 'Error Message' } }); + } else if (this.successes.length > 0) { + this.styledHeader('Permsets Assigned'); + this.table(this.successes, { name: { header: 'Username' }, value: { header: 'Permission Set Assignment' } }); + } else if (this.failures.length > 0) { + this.styledHeader('Failures'); + this.table(this.failures, { name: { header: 'Username' }, message: { header: 'Error Message' } }); + } + } + + private setExitCode(): void { + if (this.failures.length && this.successes.length) { + process.exitCode = 68; + } else if (this.failures.length) { + process.exitCode = 1; + } else if (this.successes.length) { + process.exitCode = 0; + } + } +} diff --git a/src/baseCommands/user/permsetlicense/assign.ts b/src/baseCommands/user/permsetlicense/assign.ts new file mode 100644 index 00000000..b5935643 --- /dev/null +++ b/src/baseCommands/user/permsetlicense/assign.ts @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020, 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 { Connection, Logger, Messages, SfError, StateAggregator } from '@salesforce/core'; +import { SfCommand } from '@salesforce/sf-plugins-core'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign'); + +type SuccessMsg = { + name: string; + value: string; +}; + +type FailureMsg = { + name: string; + message: string; +}; + +export type PSLResult = { + successes: SuccessMsg[]; + failures: FailureMsg[]; +}; + +interface PermissionSetLicense { + Id: string; +} + +export abstract class UserPermSetLicenseAssignBaseCommand extends SfCommand { + protected usernamesOrAliases: string[] = []; + protected pslName: string; + protected connection: Connection; + + private logger: Logger; + private readonly successes: SuccessMsg[] = []; + private readonly failures: FailureMsg[] = []; + private pslId: string; + + public async assign(): Promise { + this.logger = await Logger.child(this.constructor.name); + this.logger.debug(`will assign perm set license "${this.pslName}" to users: ${this.usernamesOrAliases.join(', ')}`); + try { + this.pslId = ( + await this.connection.singleRecordQuery( + `select Id from PermissionSetLicense where DeveloperName = '${this.pslName}' or MasterLabel = '${this.pslName}'` + ) + ).Id; + } catch { + throw new SfError('PermissionSetLicense not found'); + } + ( + await Promise.all( + this.usernamesOrAliases.map((usernameOrAlias) => + this.usernameToPSLAssignment({ + pslName: this.pslName, + usernameOrAlias, + }) + ) + ) + ).map((result) => { + if (isSuccess(result)) { + this.successes.push(result); + } else { + this.failures.push(result); + } + }); + + this.print(); + this.setExitCode(); + + return { + successes: this.successes, + failures: this.failures, + }; + } + + // handles one username/psl combo so these can run in parallel + private async usernameToPSLAssignment({ + pslName, + usernameOrAlias, + }: { + pslName: string; + usernameOrAlias: string; + }): Promise { + // Convert any aliases to usernames + const resolvedUsername = (await StateAggregator.getInstance()).aliases.resolveUsername(usernameOrAlias); + + try { + const AssigneeId = ( + await this.connection.singleRecordQuery<{ Id: string }>( + `select Id from User where Username = '${resolvedUsername}'` + ) + ).Id; + + await this.connection.sobject('PermissionSetLicenseAssign').create({ + AssigneeId, + PermissionSetLicenseId: this.pslId, + }); + return { + name: resolvedUsername, + value: pslName, + }; + } catch (e) { + // idempotency. If user(s) already have PSL, the API will throw an error about duplicate value. + // but we're going to call that a success + if (e instanceof Error && e.message.startsWith('duplicate value found')) { + this.warn(messages.getMessage('duplicateValue', [resolvedUsername, pslName])); + return { + name: resolvedUsername, + value: pslName, + }; + } else { + return { + name: resolvedUsername, + message: e instanceof Error ? e.message : 'error contained no message', + }; + } + } + } + + private setExitCode(): void { + if (this.failures.length && this.successes.length) { + process.exitCode = 68; + } else if (this.failures.length) { + process.exitCode = 1; + } else if (this.successes.length) { + process.exitCode = 0; + } + } + + private print(): void { + if (this.failures.length > 0 && this.successes.length > 0) { + this.styledHeader('Partial Success'); + } + if (this.successes.length > 0) { + this.styledHeader('Permset Licenses Assigned'); + this.table(this.successes, { + name: { header: 'Username' }, + value: { header: 'Permission Set License Assignment' }, + }); + } + + if (this.failures.length > 0) { + if (this.successes.length > 0) { + this.log(''); + } + + this.styledHeader('Failures'); + this.table(this.failures, { name: { header: 'Username' } }, { message: { header: 'Error Message' } }); + } + } +} + +const isSuccess = (input: SuccessMsg | FailureMsg): input is SuccessMsg => (input as SuccessMsg).value !== undefined; diff --git a/src/commands/force/user/password/generate.ts b/src/commands/force/user/password/generate.ts index 22139c0b..012e3bb3 100644 --- a/src/commands/force/user/password/generate.ts +++ b/src/commands/force/user/password/generate.ts @@ -4,30 +4,30 @@ * 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 * as os from 'os'; -import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; -import { AuthInfo, Connection, StateAggregator, Messages, Org, SfError, User } from '@salesforce/core'; -import { PasswordConditions } from '@salesforce/core/lib/org/user'; -import { asNumber } from '@salesforce/ts-types'; +import { + arrayWithDeprecation, + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + requiredHubFlagWithDeprecations, +} from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { ensureArray } from '@salesforce/kit'; +import { GenerateResult, UserPasswordGenerateBaseCommand } from '../../../../baseCommands/user/password/generate'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'password.generate'); -interface PasswordData { - username?: string; - password: string; -} - -export class UserPasswordGenerateCommand extends SfdxCommand { +export class ForceUserPasswordGenerateCommand extends UserPasswordGenerateBaseCommand { + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); - public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly supportsDevhubUsername = true; - public static readonly flagsConfig: FlagsConfig = { - onbehalfof: flags.array({ + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + 'on-behalf-of': arrayWithDeprecation({ + aliases: ['onbehalfof'], char: 'o', description: messages.getMessage('flags.onBehalfOf'), }), - length: flags.integer({ + length: Flags.integer({ char: 'l', description: messages.getMessage('flags.length'), min: 8, @@ -35,97 +35,32 @@ export class UserPasswordGenerateCommand extends SfdxCommand { default: 13, }), // the higher the value, the stronger the password - complexity: flags.integer({ + complexity: Flags.integer({ char: 'c', description: messages.getMessage('flags.complexity'), min: 0, max: 5, default: 5, }), + 'target-dev-hub': { ...requiredHubFlagWithDeprecations, required: false }, + 'target-org': Flags.requiredOrg({ + char: 'u', + summary: messages.getMessage('flags.target-org.summary'), + aliases: ['targetusername'], + required: true, + }), + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, }; - private usernames: string[]; - private passwordData: PasswordData[] = []; - - public async run(): Promise { - this.ux.warn('The --targetdevhubusername flag is deprecated and will be removed in v57 or later.'); - this.usernames = (this.flags.onbehalfof as string[]) ?? [this.org.getUsername()]; - - const passwordCondition: PasswordConditions = { - length: asNumber(this.flags.length, 13), - complexity: asNumber(this.flags.complexity, 5), - }; - - // sequentially to avoid auth file collisions until configFile if safer - /* eslint-disable no-await-in-loop */ - for (const aliasOrUsername of this.usernames) { - try { - // Convert any aliases to usernames - // fetch will return undefined if there's no Alias for that name - const username = (await StateAggregator.getInstance()).aliases.resolveUsername(aliasOrUsername); - - const authInfo: AuthInfo = await AuthInfo.create({ username }); - const connection: Connection = await Connection.create({ authInfo }); - const org = await Org.create({ connection }); - const user: User = await User.create({ org }); - const password = User.generatePasswordUtf8(passwordCondition); - // we only need the Id, so instead of User.retrieve we'll just query - // this avoids permission issues if ProfileId is restricted for the user querying for it - const result: { Id: string } = await connection.singleRecordQuery( - `SELECT Id FROM User WHERE Username='${username}'` - ); - - // userId is used by `assignPassword` so we need to set it here - authInfo.getFields().userId = result.Id; - await user.assignPassword(authInfo, password); - - password.value((pass) => { - this.passwordData.push({ username, password: pass.toString('utf-8') }); - authInfo.update({ password: pass.toString('utf-8') }); - }); - - await authInfo.save(); - } catch (e) { - const err = e as SfError; - if ( - err.message.includes('Cannot set password for self') || - err.message.includes('The requested Resource does not exist') - ) { - // we don't have access to the apiVersion from what happened in the try, so until v51 is r2, we have to check versions the hard way - const authInfo: AuthInfo = await AuthInfo.create({ username: aliasOrUsername }); - const connection: Connection = await Connection.create({ authInfo }); - const org = await Org.create({ connection }); - if (parseInt(await org.retrieveMaxApiVersion(), 10) >= 51) { - throw new SfError( - messages.getMessage('noSelfSetError'), - 'noSelfSetErrorError', - messages.getMessage('noSelfSetErrorActions').split(os.EOL) - ); - } - throw new SfError(messages.getMessage('noSelfSetErrorV50'), 'noSelfSetErrorError'); - } - throw SfError.wrap(err); - } - } - /* eslint-enable no-await-in-loop */ - - this.print(); - - return this.passwordData.length === 1 ? this.passwordData[0] : this.passwordData; - } - - private print(): void { - if (this.passwordData) { - const successMsg = messages.getMessage('success', [this.passwordData[0].password, this.passwordData[0].username]); - const viewMsg = messages.getMessage('viewWithCommand', [this.passwordData[0].username]); - this.ux.log(`${successMsg}${os.EOL}${viewMsg}`); - } else { - this.ux.log(messages.getMessage('successMultiple', [os.EOL])); - const columnData = { - username: { header: 'USERNAME' }, - password: { header: 'PASSWORD' }, - }; - this.ux.table(this.passwordData, columnData); - } + public async run(): Promise { + const { flags } = await this.parse(ForceUserPasswordGenerateCommand); + this.warn('The --target-dev-hub flag is deprecated and will be removed in v57 or later.'); + this.usernames = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + this.length = flags.length; + this.complexity = flags.complexity; + this.org = flags['target-org']; + this.connection = this.org.getConnection(flags['api-version']); + return this.generate(); } } diff --git a/src/commands/force/user/permset/assign.ts b/src/commands/force/user/permset/assign.ts index 369d5c5b..87a1203a 100644 --- a/src/commands/force/user/permset/assign.ts +++ b/src/commands/force/user/permset/assign.ts @@ -5,121 +5,47 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as os from 'os'; -import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; -import { Connection, StateAggregator, Messages, Org, SfError, User } from '@salesforce/core'; +import { Messages, Org } from '@salesforce/core'; +import { Flags, arrayWithDeprecation, loglevel, orgApiVersionFlagWithDeprecations } from '@salesforce/sf-plugins-core'; +import { ensureArray } from '@salesforce/kit'; +import { PermsetAssignResult, UserPermSetAssignBaseCommand } from '../../../../baseCommands/user/permset/assign'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'permset.assign'); -type SuccessMsg = { - name: string; - value: string; -}; - -type FailureMsg = { - name: string; - message: string; -}; - -export type PermsetAssignResult = { - successes: SuccessMsg[]; - failures: FailureMsg[]; -}; - -export class UserPermsetAssignCommand extends SfdxCommand { +export class ForceUserPermSetAssignCommand extends UserPermSetAssignBaseCommand { + public static readonly hidden = true; + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); - public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly flagsConfig: FlagsConfig = { - permsetname: flags.array({ + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + 'perm-set-name': arrayWithDeprecation({ + aliases: ['permsetname', 'name'], char: 'n', description: messages.getMessage('flags.permsetName'), required: true, }), - onbehalfof: flags.array({ + 'on-behalf-of': arrayWithDeprecation({ char: 'o', description: messages.getMessage('flags.onBehalfOf'), + aliases: ['onbehalfof'], }), + 'target-org': Flags.requiredOrg({ + char: 'u', + summary: messages.getMessage('flags.target-org.summary'), + aliases: ['targetusername'], + required: true, + }), + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, }; - private readonly successes: SuccessMsg[] = []; - private readonly failures: FailureMsg[] = []; public async run(): Promise { - try { - const aliasOrUsernames = (this.flags.onbehalfof as string[]) ?? [this.org.getUsername()]; - - const connection: Connection = this.org.getConnection(); - const org = await Org.create({ connection }); - - // sequentially to avoid auth file collisions until configFile if safer - /* eslint-disable no-await-in-loop */ - for (const aliasOrUsername of aliasOrUsernames) { - // Attempt to convert any aliases to usernames. Not found alias will be **assumed** to be a username - const username = (await StateAggregator.getInstance()).aliases.resolveUsername(aliasOrUsername); - const user: User = await User.create({ org }); - // get userId of whomever the permset will be assigned to via query to avoid AuthInfo if remote user - const queryResult = await connection.singleRecordQuery<{ Id: string }>( - `SELECT Id FROM User WHERE Username='${username}'` - ); - // this is hard to parallelize because core returns void instead of some result object we can handle. Promise.allSettled might work - for (const permsetName of this.flags.permsetname as string[]) { - try { - await user.assignPermissionSets(queryResult.Id, [permsetName]); - this.successes.push({ - name: aliasOrUsername, - value: permsetName, - }); - } catch (e) { - const err = e as SfError; - this.failures.push({ - name: aliasOrUsername, - message: err.message, - }); - } - } - } - /* eslint-enable no-await-in-loop */ - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - throw SfError.wrap(e); - } - throw e; - } - - this.print(); - this.setExitCode(); - - return { - successes: this.successes, - failures: this.failures, - }; - } - - private print(): void { - if (this.failures.length > 0 && this.successes.length > 0) { - this.ux.styledHeader('Partial Success'); - this.ux.styledHeader('Permsets Assigned'); - this.ux.table(this.successes, { name: { header: 'Username' }, value: { header: 'Permission Set Assignment' } }); - this.ux.log(''); - this.ux.styledHeader('Failures'); - this.ux.table(this.failures, { name: { header: 'Username' }, message: { header: 'Error Message' } }); - } else if (this.successes.length > 0) { - this.ux.styledHeader('Permsets Assigned'); - this.ux.table(this.successes, { name: { header: 'Username' }, value: { header: 'Permission Set Assignment' } }); - } else if (this.failures.length > 0) { - this.ux.styledHeader('Failures'); - this.ux.table(this.failures, { name: { header: 'Username' }, message: { header: 'Error Message' } }); - } - } - - private setExitCode(): void { - if (this.failures.length && this.successes.length) { - process.exitCode = 68; - } else if (this.failures.length) { - process.exitCode = 1; - } else if (this.successes.length) { - process.exitCode = 0; - } + const { flags } = await this.parse(ForceUserPermSetAssignCommand); + this.aliasOrUsernames = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + this.permSetNames = flags['perm-set-name']; + this.connection = flags['target-org'].getConnection(flags['api-version']); + this.org = await Org.create({ connection: this.connection }); + return this.assign(); } } diff --git a/src/commands/force/user/permsetlicense/assign.ts b/src/commands/force/user/permsetlicense/assign.ts index 482d1f8e..4e368650 100644 --- a/src/commands/force/user/permsetlicense/assign.ts +++ b/src/commands/force/user/permsetlicense/assign.ts @@ -5,161 +5,46 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as os from 'os'; -import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; -import { StateAggregator, Messages, SfError } from '@salesforce/core'; +import { Messages } from '@salesforce/core'; +import { arrayWithDeprecation, Flags, loglevel, orgApiVersionFlagWithDeprecations } from '@salesforce/sf-plugins-core'; +import { ensureArray } from '@salesforce/kit'; +import { PSLResult, UserPermSetLicenseAssignBaseCommand } from '../../../../baseCommands/user/permsetlicense/assign'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign'); -type SuccessMsg = { - name: string; - value: string; -}; - -type FailureMsg = { - name: string; - message: string; -}; - -export type PSLResult = { - successes: SuccessMsg[]; - failures: FailureMsg[]; -}; - -interface PermissionSetLicense { - Id: string; -} - -export class UserPermsetLicenseAssignCommand extends SfdxCommand { +export class ForceUserPermSetLicenseAssignCommand extends UserPermSetLicenseAssignBaseCommand { + public static readonly hidden = true; + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); - public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly flagsConfig: FlagsConfig = { - name: flags.string({ + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + name: Flags.string({ char: 'n', description: messages.getMessage('flags.name'), required: true, + aliases: ['perm-set-license', 'psl'], }), - onbehalfof: flags.array({ - char: 'o', + 'on-behalf-of': arrayWithDeprecation({ + char: 'b', description: messages.getMessage('flags.onBehalfOf'), + aliases: ['onbehalfof'], }), + 'target-org': Flags.requiredOrg({ + char: 'u', + summary: messages.getMessage('flags.target-org.summary'), + aliases: ['targetusername'], + required: true, + }), + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, }; - private readonly successes: SuccessMsg[] = []; - private readonly failures: FailureMsg[] = []; - private pslId: string; public async run(): Promise { - const usernamesOrAliases = (this.flags.onbehalfof as string[]) ?? [this.org.getUsername()]; - this.logger.debug(`will assign permset to users: ${usernamesOrAliases.join(', ')}`); - const pslName = this.flags.name as string; - const conn = this.org.getConnection(); - try { - this.pslId = ( - await conn.singleRecordQuery( - `select Id from PermissionSetLicense where DeveloperName = '${pslName}' or MasterLabel = '${pslName}'` - ) - ).Id; - } catch { - throw new SfError('PermissionSetLicense not found'); - } - ( - await Promise.all( - usernamesOrAliases.map((usernameOrAlias) => this.usernameToPSLAssignment({ pslName, usernameOrAlias })) - ) - ).map((result) => { - if (isSuccess(result)) { - this.successes.push(result); - } else { - this.failures.push(result); - } - }); - - this.print(); - this.setExitCode(); - - return { - successes: this.successes, - failures: this.failures, - }; - } - - // handles one username/psl combo so these can run in parallel - private async usernameToPSLAssignment({ - pslName, - usernameOrAlias, - }: { - pslName: string; - usernameOrAlias: string; - }): Promise { - // Convert any aliases to usernames - const resolvedUsername = (await StateAggregator.getInstance()).aliases.resolveUsername(usernameOrAlias); - - try { - const AssigneeId = ( - await this.org - .getConnection() - .singleRecordQuery<{ Id: string }>(`select Id from User where Username = '${resolvedUsername}'`) - ).Id; - - await this.org.getConnection().sobject('PermissionSetLicenseAssign').create({ - AssigneeId, - PermissionSetLicenseId: this.pslId, - }); - return { - name: resolvedUsername, - value: pslName, - }; - } catch (e) { - // idempotency. If user(s) already have PSL, the API will throw an error about duplicate value. - // but we're going to call that a success - if (e instanceof Error && e.message.startsWith('duplicate value found')) { - this.ux.warn(messages.getMessage('duplicateValue', [resolvedUsername, pslName])); - return { - name: resolvedUsername, - value: pslName, - }; - } else { - return { - name: resolvedUsername, - message: e instanceof Error ? e.message : 'error contained no message', - }; - } - } - } - - private setExitCode(): void { - if (this.failures.length && this.successes.length) { - process.exitCode = 68; - } else if (this.failures.length) { - process.exitCode = 1; - } else if (this.successes.length) { - process.exitCode = 0; - } - } - - private print(): void { - if (this.failures.length > 0 && this.successes.length > 0) { - this.ux.styledHeader('Partial Success'); - } - if (this.successes.length > 0) { - this.ux.styledHeader('Permset Licenses Assigned'); - this.ux.table(this.successes, { - name: { header: 'Username' }, - value: { header: 'Permission Set License Assignment' }, - }); - } - - if (this.failures.length > 0) { - if (this.successes.length > 0) { - this.ux.log(''); - } - - this.ux.styledHeader('Failures'); - this.ux.table(this.failures, { name: { header: 'Username' } }, { message: { header: 'Error Message' } }); - } + const { flags } = await this.parse(ForceUserPermSetLicenseAssignCommand); + this.usernamesOrAliases = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + this.pslName = flags.name; + this.connection = flags['target-org'].getConnection(flags['api-version']); + return this.assign(); } } - -const isSuccess = (input: SuccessMsg | FailureMsg): input is SuccessMsg => (input as SuccessMsg).value !== undefined; diff --git a/src/commands/force/user/create.ts b/src/commands/user/create.ts similarity index 65% rename from src/commands/force/user/create.ts rename to src/commands/user/create.ts index 48546cfb..871d99a0 100644 --- a/src/commands/force/user/create.ts +++ b/src/commands/user/create.ts @@ -5,68 +5,88 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import * as os from 'os'; -import * as fse from 'fs-extra'; +import * as fs from 'fs'; import { AuthInfo, Connection, DefaultUserFields, - StateAggregator, Logger, Messages, REQUIRED_FIELDS, SfError, + StateAggregator, User, UserFields, } from '@salesforce/core'; -import { omit, mapKeys, toBoolean } from '@salesforce/kit'; -import { getString, Dictionary, isArray } from '@salesforce/ts-types'; -import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; +import { mapKeys, omit, parseJson, toBoolean } from '@salesforce/kit'; +import { Dictionary, ensureString, getString, isArray } from '@salesforce/ts-types'; +import { + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + parseVarArgs, + requiredHubFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; +import { Interfaces } from '@oclif/core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'create'); -interface SuccessMsg { +type SuccessMsg = { name: string; value: string; -} +}; -interface FailureMsg { +type FailureMsg = { name: string; message: string; -} +}; -const permsetsStringToArray = (fieldsPermsets: string | string[]): string[] => { +const permsetsStringToArray = (fieldsPermsets: string | string[] | undefined): string[] => { if (!fieldsPermsets) return []; - return isArray(fieldsPermsets) + return isArray(fieldsPermsets) ? fieldsPermsets : fieldsPermsets.split(',').map((item) => item.replace("'", '').trim()); }; -export class UserCreateCommand extends SfdxCommand { +export class UserCreateCommand extends SfCommand { + public static strict = false; + public static readonly aliases = ['force:user:create', 'org:create:user']; + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly requiresDevhubUsername = true; - public static readonly varargs = true; - public static readonly flagsConfig: FlagsConfig = { - setalias: flags.string({ + public static readonly flags = { + 'set-alias': Flags.string({ char: 'a', - description: messages.getMessage('flags.alias'), + summary: messages.getMessage('flags.alias.summary'), + aliases: ['setalias'], }), - definitionfile: flags.string({ + 'definition-file': Flags.string({ char: 'f', - description: messages.getMessage('flags.definitionfile'), + summary: messages.getMessage('flags.definitionfile.summary'), + aliases: ['definitionfile'], }), - setuniqueusername: flags.boolean({ + 'set-unique-username': Flags.boolean({ char: 's', - description: messages.getMessage('flags.setuniqueusername'), + summary: messages.getMessage('flags.setuniqueusername.summary'), + aliases: ['setuniqueusername'], }), + 'target-dev-hub': requiredHubFlagWithDeprecations, + 'target-org': requiredOrgFlagWithDeprecations, + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, }; - private user: User; + private targetOrgUser: User; private successes: SuccessMsg[] = []; private failures: FailureMsg[] = []; - private authInfo: AuthInfo; + private newUserAuthInfo: AuthInfo; + private logger: Logger; + private flags: Interfaces.InferredFlags; + // eslint-disable-next-line sf-plugin/no-deprecated-properties + private varargs: Record; /** * removes fields that cause errors in salesforce APIs within sfdx-core's createUser method @@ -79,17 +99,20 @@ export class UserCreateCommand extends SfdxCommand { } public async run(): Promise { + const { flags, argv } = await this.parse(UserCreateCommand); + this.varargs = parseVarArgs({}, argv); + this.flags = flags as Interfaces.InferredFlags; this.logger = await Logger.child(this.constructor.name); const defaultUserFields: DefaultUserFields = await DefaultUserFields.create({ - templateUser: this.org.getUsername(), + templateUser: this.flags['target-org'].getUsername() ?? '', }); - this.user = await User.create({ org: this.org }); + this.targetOrgUser = await User.create({ org: this.flags['target-org'] }); // merge defaults with provided values with cli > file > defaults const fields = await this.aggregateFields(defaultUserFields.getFields()); try { - this.authInfo = await this.user.createUser(UserCreateCommand.stripInvalidAPIFields(fields)); + this.newUserAuthInfo = await this.targetOrgUser.createUser(UserCreateCommand.stripInvalidAPIFields(fields)); } catch (e) { if (!(e instanceof Error)) { throw e; @@ -97,7 +120,7 @@ export class UserCreateCommand extends SfdxCommand { await this.catchCreateUser(e, fields); } - if (fields.profileName) await this.authInfo.save({ userProfileName: fields.profileName }); + if (fields.profileName) await this.newUserAuthInfo.save({ userProfileName: fields.profileName }); // Assign permission sets to the created user if (fields.permsets) { @@ -106,7 +129,10 @@ export class UserCreateCommand extends SfdxCommand { // it will either be a comma separated string, or an array, force it into an array const permsetArray = permsetsStringToArray(fields.permsets); - await this.user.assignPermissionSets(this.authInfo.getFields().userId, permsetArray); + await this.targetOrgUser.assignPermissionSets( + ensureString(this.newUserAuthInfo.getFields().userId), + permsetArray + ); this.successes.push({ name: 'Permission Set Assignment', value: permsetArray.join(','), @@ -124,10 +150,10 @@ export class UserCreateCommand extends SfdxCommand { if (fields.generatePassword) { try { const password = User.generatePasswordUtf8(); - await this.user.assignPassword(this.authInfo, password); + await this.targetOrgUser.assignPassword(this.newUserAuthInfo, password); password.value((pass: Buffer) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.authInfo.save({ password: pass.toString('utf-8') }); + this.newUserAuthInfo.save({ password: pass.toString('utf-8') }); this.successes.push({ name: 'Password Assignment', value: pass.toString(), @@ -143,19 +169,19 @@ export class UserCreateCommand extends SfdxCommand { } // Set the alias if specified - if (this.flags.setalias && typeof this.flags.setalias === 'string') { + if (this.flags['set-alias']) { const stateAggregator = await StateAggregator.getInstance(); - stateAggregator.aliases.set(this.flags.setalias, fields.username); + stateAggregator.aliases.set(this.flags['set-alias'], fields.username); await stateAggregator.aliases.write(); } - fields.id = this.authInfo.getFields().userId; + fields.id = ensureString(this.newUserAuthInfo.getFields().userId); this.print(fields); this.setExitCode(); const { permsets, ...fieldsWithoutPermsets } = fields; return { - orgId: this.org.getOrgId(), + orgId: this.flags['target-org'].getOrgId(), permissionSetAssignments: permsetsStringToArray(permsets), fields: { ...mapKeys(fieldsWithoutPermsets, (value, key) => key.toLowerCase()) }, }; @@ -164,8 +190,8 @@ export class UserCreateCommand extends SfdxCommand { private async catchCreateUser(respBody: Error, fields: UserFields): Promise { // For Gacks, the error message is on response.body[0].message but for handled errors // the error message is on response.body.Errors[0].description. - const errMessage = getString(respBody, 'message') || 'Unknown Error'; - const conn: Connection = this.org.getConnection(); + const errMessage = getString(respBody, 'message') ?? 'Unknown Error'; + const conn: Connection = this.flags['target-org'].getConnection(this.flags['api-version']); // Provide a more user friendly error message for certain server errors. if (errMessage.includes('LICENSE_LIMIT_EXCEEDED')) { @@ -184,10 +210,10 @@ export class UserCreateCommand extends SfdxCommand { // username can be overridden both in the file or varargs, save it to check if it was changed somewhere const defaultUsername = defaultFields.username; - // start with the default fields, then add the fields from the file, then (possibly overwritting) add the fields from the cli varargs param - if (this.flags.definitionfile && typeof this.flags.definitionfile === 'string') { + // start with the default fields, then add the fields from the file, then (possibly overwriting) add the fields from the cli varargs param + if (this.flags['definition-file']) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call - const content = (await fse.readJson(this.flags.definitionfile)) as UserFields; + const content = parseJson(await fs.promises.readFile(this.flags['definition-file'], 'utf-8')) as UserFields; Object.keys(content).forEach((key) => { // cast entries to lowercase to standardize defaultFields[lowerFirstLetter(key)] = content[key] as keyof typeof REQUIRED_FIELDS; @@ -209,17 +235,17 @@ export class UserCreateCommand extends SfdxCommand { }); } - // check if "username" was passed along with "setuniqueusername" flag, if so append org id - if (this.flags.setuniqueusername && defaultFields.username !== defaultUsername) { - defaultFields.username = `${defaultFields.username}.${this.org.getOrgId().toLowerCase()}`; + // check if "username" was passed along with "set-unique-username" flag, if so append org id + if (this.flags['set-unique-username'] && defaultFields.username !== defaultUsername) { + defaultFields.username = `${defaultFields.username}.${this.flags['target-org'].getOrgId().toLowerCase()}`; } // 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; this.logger.debug(`Querying org for profile name [${name}]`); - const profile = await this.org - .getConnection() + const profile = await this.flags['target-org'] + .getConnection(this.flags['api-version']) .singleRecordQuery<{ Id: string }>(`SELECT id FROM profile WHERE name='${name}'`); defaultFields.profileId = profile.Id; } @@ -231,7 +257,7 @@ export class UserCreateCommand extends SfdxCommand { const userCreatedSuccessMsg = messages.getMessage('success', [ fields.username, fields.id, - this.org.getOrgId(), + this.flags['target-org'].getOrgId(), os.EOL, fields.username, ]); @@ -239,13 +265,13 @@ export class UserCreateCommand extends SfdxCommand { // we initialize to be an empty array to be able to push onto it // so we need to check that the size is greater than 0 to know we had a failure if (this.failures.length > 0) { - this.ux.styledHeader('Partial Success'); - this.ux.log(userCreatedSuccessMsg); - this.ux.log(''); - this.ux.styledHeader('Failures'); - this.ux.table(this.failures, { name: { header: 'Action' }, message: { header: 'Error Message' } }); + this.styledHeader('Partial Success'); + this.log(userCreatedSuccessMsg); + this.log(''); + this.styledHeader('Failures'); + this.table(this.failures, { name: { header: 'Action' }, message: { header: 'Error Message' } }); } else { - this.ux.log(userCreatedSuccessMsg); + this.log(userCreatedSuccessMsg); } } diff --git a/src/commands/force/user/display.ts b/src/commands/user/display.ts similarity index 57% rename from src/commands/force/user/display.ts rename to src/commands/user/display.ts index 89c094d8..5c2b668c 100644 --- a/src/commands/force/user/display.ts +++ b/src/commands/user/display.ts @@ -6,9 +6,16 @@ */ import * as os from 'os'; -import { SfdxCommand } from '@salesforce/command'; -import { AuthFields, AuthInfo, Connection, Logger, Messages, SfError, sfdc, StateAggregator } from '@salesforce/core'; -import { getString } from '@salesforce/ts-types'; +import { AuthFields, Connection, Logger, Messages, StateAggregator } from '@salesforce/core'; +import { ensureString, getString } from '@salesforce/ts-types'; +import { + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + requiredHubFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'display'); @@ -18,43 +25,49 @@ export type UserDisplayResult = { profileName: string; id: string; orgId: string; - accessToken: string; - instanceUrl: string; - loginUrl: string; + accessToken?: string; + instanceUrl?: string; + loginUrl?: string; alias?: string; password?: string; }; -export class UserDisplayCommand extends SfdxCommand { +export class UserDisplayCommand extends SfCommand { + public static readonly aliases = ['force:user:display', 'org:display:user']; + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly supportsDevhubUsername = true; + public static readonly flags = { + 'target-dev-hub': { ...requiredHubFlagWithDeprecations, required: false }, + 'target-org': requiredOrgFlagWithDeprecations, + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, + verbose: Flags.boolean(), + }; + + private logger: Logger; public async run(): Promise { - this.ux.warn('The --targetdevhubusername flag is deprecated and will be removed in v57 or later.'); + const { flags } = await this.parse(UserDisplayCommand); + this.warn('The --targetdevhubusername flag is deprecated and will be removed in v57 or later.'); this.logger = await Logger.child(this.constructor.name); - if (sfdc.matchesAccessToken(this.flags.targetusername as string)) { - throw new SfError(messages.getMessage('accessTokenError'), 'accessTokenErrorError', [ - messages.getMessage('accessTokenAction'), - ]); - } - const username: string = this.org.getUsername(); - const userAuthDataArray: AuthInfo[] = await this.org.readUserAuthFiles(); + + const username = ensureString(flags['target-org'].getUsername()); + const userAuthDataArray = await flags['target-org'].readUserAuthFiles(); // userAuthDataArray contains all of the Org's users AuthInfo, we just need the default or -u, which is in the username variable - const userAuthData: AuthFields = userAuthDataArray + const userAuthData: AuthFields | undefined = userAuthDataArray .find((uat) => uat.getFields().username === username) - .getFields(true); - const conn: Connection = this.org.getConnection(); + ?.getFields(true); + const conn: Connection = flags['target-org'].getConnection(flags['api-version']); - let profileName: string = userAuthData.userProfileName; - let userId: string = userAuthData.userId; + let profileName = userAuthData?.userProfileName; + let userId = userAuthData?.userId; try { // the user executing this command may not have access to the Profile sObject. if (!profileName) { const PROFILE_NAME_QUERY = `SELECT name FROM Profile WHERE Id IN (SELECT ProfileId FROM User WHERE username='${username}')`; - profileName = getString(await conn.query(PROFILE_NAME_QUERY), 'records[0].Name'); + profileName = getString(await conn.query(PROFILE_NAME_QUERY), 'records[0].Name') as string; } } catch (err) { profileName = 'unknown'; @@ -66,7 +79,7 @@ export class UserDisplayCommand extends SfdxCommand { try { if (!userId) { const USER_QUERY = `SELECT id FROM User WHERE username='${username}'`; - userId = getString(await conn.query(USER_QUERY), 'records[0].Id'); + userId = getString(await conn.query(USER_QUERY), 'records[0].Id') as string; } } catch (err) { userId = 'unknown'; @@ -76,11 +89,11 @@ export class UserDisplayCommand extends SfdxCommand { } const result: UserDisplayResult = { - accessToken: conn.accessToken, + accessToken: conn.accessToken as string, id: userId, - instanceUrl: userAuthData.instanceUrl, - loginUrl: userAuthData.loginUrl, - orgId: this.org.getOrgId(), + instanceUrl: userAuthData?.instanceUrl, + loginUrl: userAuthData?.loginUrl, + orgId: flags['target-org'].getOrgId(), profileName, username, }; @@ -91,12 +104,12 @@ export class UserDisplayCommand extends SfdxCommand { result.alias = alias; } - if (userAuthData.password) { + if (userAuthData?.password) { result.password = userAuthData.password; } - this.ux.warn(messages.getMessage('securityWarning')); - this.ux.log(''); + this.warn(messages.getMessage('securityWarning')); + this.log(''); this.print(result); return result; @@ -107,20 +120,20 @@ export class UserDisplayCommand extends SfdxCommand { key: { header: 'key' }, label: { header: 'label' }, }; - - const tableRow = []; + type TT = { key: string; label: string }; + const tableRow: TT[] = []; // to get proper capitalization and spacing, enter the rows - tableRow.push({ key: 'Username', label: result.username }); + tableRow.push({ key: 'Username', label: result.username ?? 'unknown' }); tableRow.push({ key: 'Profile Name', label: result.profileName }); tableRow.push({ key: 'Id', label: result.id }); tableRow.push({ key: 'Org Id', label: result.orgId }); - tableRow.push({ key: 'Access Token', label: result.accessToken }); - tableRow.push({ key: 'Instance Url', label: result.instanceUrl }); - tableRow.push({ key: 'Login Url', label: result.loginUrl }); + tableRow.push({ key: 'Access Token', label: result.accessToken ?? '' }); + tableRow.push({ key: 'Instance Url', label: result.instanceUrl ?? '' }); + tableRow.push({ key: 'Login Url', label: result.loginUrl ?? '' }); if (result.alias) tableRow.push({ key: 'Alias', label: result.alias }); if (result.password) tableRow.push({ key: 'Password', label: result.password }); - this.ux.styledHeader('User Description'); - this.ux.table(tableRow, columns); + this.styledHeader('User Description'); + this.table(tableRow, columns); } } diff --git a/src/commands/force/user/list.ts b/src/commands/user/list.ts similarity index 62% rename from src/commands/force/user/list.ts rename to src/commands/user/list.ts index 8288b38f..5633db5c 100644 --- a/src/commands/force/user/list.ts +++ b/src/commands/user/list.ts @@ -5,13 +5,19 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import * as os from 'os'; -import { SfdxCommand } from '@salesforce/command'; -import { Messages, Connection, StateAggregator } from '@salesforce/core'; +import { Connection, Messages, Org, StateAggregator } from '@salesforce/core'; +import { + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'list'); -export type AuthList = { +export type AuthList = Partial<{ defaultMarker: string; alias: string; username: string; @@ -21,23 +27,35 @@ export type AuthList = { instanceUrl: string; loginUrl: string; userId: string; -}; +}>; + +export type UserList = AuthList[]; type UserInfo = { Username: string; ProfileId: string; Id: string }; type UserInfoMap = Record; type ProfileInfo = { Id: string; Name: string }; type ProfileInfoMap = Record; -export class UserListCommand extends SfdxCommand { +export class UserListCommand extends SfCommand { + public static readonly aliases = ['force:user:list', 'org:list:users']; + public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly requiresUsername = true; - public static readonly supportsDevhubUsername = true; + public static readonly flags = { + 'target-dev-hub': Flags.optionalOrg({ char: 'v', summary: messages.getMessage('flags.target-hub.summary') }), + 'target-org': requiredOrgFlagWithDeprecations, + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, + }; + private conn: Connection; + private org: Org; - public async run(): Promise { - this.ux.warn('The --targetdevhubusername flag is deprecated and will be removed in v57 or later.'); - this.conn = this.org.getConnection(); + public async run(): Promise { + const { flags } = await this.parse(UserListCommand); + this.warn('The --targetdevhubusername flag is deprecated and will be removed in v57 or later.'); + this.org = flags['target-org']; + this.conn = flags['target-org'].getConnection(flags['api-version']); // parallelize 2 org queries and 2 fs operations const [userInfos, profileInfos, userAuthData, aliases] = await Promise.all([ this.buildUserInfos(), @@ -46,16 +64,18 @@ export class UserListCommand extends SfdxCommand { (await StateAggregator.getInstance()).aliases, ]); - const authList: AuthList[] = userAuthData.map((authData) => { + const authList: UserList = userAuthData.map((authData) => { const username = authData.getUsername(); - // if they passed in a alias and it maps to something we have an Alias. + // if they passed in an alias and it maps to something we have an Alias. const alias = aliases.get(username); + const userInfo = userInfos[username]; + const profileName = userInfo && profileInfos[userInfo.ProfileId]; return { - defaultMarker: this.org.getUsername() === username ? '(A)' : '', - alias: alias || '', + defaultMarker: flags['target-org']?.getUsername() === username ? '(A)' : '', + alias: alias ?? '', username, - profileName: profileInfos[userInfos[username].ProfileId], - orgId: this.org.getOrgId(), + profileName, + orgId: flags['target-org']?.getOrgId(), accessToken: authData.getFields().accessToken, instanceUrl: authData.getFields().instanceUrl, loginUrl: authData.getFields().loginUrl, @@ -71,8 +91,8 @@ export class UserListCommand extends SfdxCommand { userId: { header: 'User Id' }, }; - this.ux.styledHeader(`Users in org ${this.org.getOrgId()}`); - this.ux.table(authList, columns); + this.styledHeader(`Users in org ${flags['target-org']?.getOrgId()}`); + this.table(authList, columns); return authList; } @@ -92,6 +112,7 @@ export class UserListCommand extends SfdxCommand { return userInfo; }, {}); } + return {}; } /** @@ -109,5 +130,6 @@ export class UserListCommand extends SfdxCommand { return profileInfo; }, {}); } + return {}; } } diff --git a/src/commands/user/password/generate.ts b/src/commands/user/password/generate.ts new file mode 100644 index 00000000..3bce469a --- /dev/null +++ b/src/commands/user/password/generate.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020, 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 { Flags } from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { ensureArray } from '@salesforce/kit'; +import { GenerateResult, UserPasswordGenerateBaseCommand } from '../../../baseCommands/user/password/generate'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-user', 'password.generate'); + +export type PasswordData = { + username?: string; + password: string; +}; + +export class UserPasswordGenerateCommand extends UserPasswordGenerateBaseCommand { + public static readonly aliases = ['org:generate:password']; + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + 'on-behalf-of': Flags.string({ + char: 'b', + description: messages.getMessage('flags.onBehalfOf'), + multiple: true, + parse: (input): Promise => { + if (input.includes(',')) { + throw messages.createError('onBehalfOfMultipleError'); + } + return Promise.resolve(input); + }, + }), + length: Flags.integer({ + char: 'l', + description: messages.getMessage('flags.length'), + min: 8, + max: 1000, + default: 13, + }), + // the higher the value, the stronger the password + complexity: Flags.integer({ + char: 'c', + description: messages.getMessage('flags.complexity'), + min: 0, + max: 5, + default: 5, + }), + 'target-org': Flags.requiredOrg({ required: true }), + 'api-version': Flags.orgApiVersion(), + }; + + public async run(): Promise { + const { flags } = await this.parse(UserPasswordGenerateCommand); + this.usernames = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + this.length = flags.length; + this.complexity = flags.complexity; + this.org = flags['target-org']; + this.connection = this.org.getConnection(flags['api-version']); + return this.generate(); + } +} diff --git a/src/commands/user/permset/assign.ts b/src/commands/user/permset/assign.ts new file mode 100644 index 00000000..2e3cde69 --- /dev/null +++ b/src/commands/user/permset/assign.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020, 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 { Messages, Org } from '@salesforce/core'; +import { Flags } from '@salesforce/sf-plugins-core'; +import { ensureArray } from '@salesforce/kit'; +import { PermsetAssignResult, UserPermSetAssignBaseCommand } from '../../../baseCommands/user/permset/assign'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-user', 'permset.assign'); + +export class UserPermSetAssignCommand extends UserPermSetAssignBaseCommand { + public static readonly aliases = ['org:assign:permset']; + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + 'perm-set-name': Flags.string({ + aliases: ['permsetname', 'name'], + char: 'n', + description: messages.getMessage('flags.permsetName'), + required: true, + multiple: true, + }), + 'on-behalf-of': Flags.string({ + char: 'b', + description: messages.getMessage('flags.onBehalfOf'), + aliases: ['onbehalfof'], + multiple: true, + }), + 'target-org': Flags.requiredOrg({ summary: messages.getMessage('flags.target-org.summary'), required: true }), + 'api-version': Flags.orgApiVersion(), + }; + + public async run(): Promise { + const { flags } = await this.parse(UserPermSetAssignCommand); + this.aliasOrUsernames = ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()); + this.permSetNames = flags['perm-set-name']; + this.connection = flags['target-org'].getConnection(flags['api-version']); + this.org = await Org.create({ connection: this.connection }); + return this.assign(); + } +} diff --git a/src/commands/user/permsetlicense/assign.ts b/src/commands/user/permsetlicense/assign.ts new file mode 100644 index 00000000..55aead98 --- /dev/null +++ b/src/commands/user/permsetlicense/assign.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, 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 { Messages } from '@salesforce/core'; +import { arrayWithDeprecation, Flags } from '@salesforce/sf-plugins-core'; +import { ensureArray } from '@salesforce/kit'; +import { PSLResult, UserPermSetLicenseAssignBaseCommand } from '../../../baseCommands/user/permsetlicense/assign'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign'); + +export class UserPermSetLicenseAssignCommand extends UserPermSetLicenseAssignBaseCommand { + public static readonly aliases = ['org:assign:permsetlicense']; + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly flags = { + name: Flags.string({ + char: 'n', + description: messages.getMessage('flags.name'), + required: true, + aliases: ['perm-set-license', 'psl'], + }), + 'on-behalf-of': arrayWithDeprecation({ + char: 'b', + description: messages.getMessage('flags.onBehalfOf'), + aliases: ['onbehalfof'], + }), + 'target-org': Flags.requiredOrg({ summary: messages.getMessage('flags.target-org.summary'), required: true }), + 'api-version': Flags.orgApiVersion(), + }; + + public async run(): Promise { + const { flags } = await this.parse(UserPermSetLicenseAssignCommand); + const username = flags['target-org'].getUsername(); + this.usernamesOrAliases = ensureArray(flags['on-behalf-of'] ?? username); + this.pslName = flags.name; + this.connection = flags['target-org'].getConnection(flags['api-version']); + return this.assign(); + } +} diff --git a/test/allCommands.nut.ts b/test/allCommands.nut.ts index 0dad3895..56fae170 100644 --- a/test/allCommands.nut.ts +++ b/test/allCommands.nut.ts @@ -4,16 +4,17 @@ * 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 * as path from 'path'; import { use, expect } from 'chai'; import * as chaiEach from 'chai-each'; import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; -import { PermsetAssignResult } from '../src/commands/force/user/permset/assign'; -import { AuthList } from '../src/commands/force/user/list'; -import { UserCreateOutput } from '../src/commands/force/user/create'; -import { UserDisplayResult } from '../src/commands/force/user/display'; +import { PermsetAssignResult } from '../src/baseCommands/user/permset/assign'; +import { AuthList } from '../src/commands/user/list'; +import { UserCreateOutput } from '../src/commands/user/create'; +import { UserDisplayResult } from '../src/commands/user/display'; use(chaiEach); let session: TestSession; @@ -37,12 +38,12 @@ describe('verifies all commands run successfully ', () => { ], }); - execCmd('force:source:push', { cli: 'sfdx' }); + execCmd('source:push', { cli: 'sfdx' }); }); it('user display', () => { - const output = execCmd('force:user:display --json', { ensureExitCode: 0 }).jsonOutput; - expect(output.result).to.have.all.keys([ + const output = execCmd('user:display --json', { ensureExitCode: 0 }).jsonOutput; + expect(output?.result).to.have.all.keys([ 'username', 'accessToken', 'id', @@ -52,27 +53,27 @@ describe('verifies all commands run successfully ', () => { 'instanceUrl', ]); - expect(output.result.orgId).to.have.length(18); - expect(output.result.id).to.have.length(18); - expect(output.result.accessToken.startsWith(output.result.orgId.substr(0, 15))).to.be.true; - mainUserId = output.result.id; + expect(output?.result.orgId).to.have.length(18); + expect(output?.result.id).to.have.length(18); + expect(output?.result?.accessToken?.startsWith(output?.result.orgId.substr(0, 15))).to.be.true; + mainUserId = output?.result.id; }); it('assigns a permset to the default user', () => { - const output = execCmd('force:user:permset:assign -n VolunteeringApp --json', { + const output = execCmd('user:permset:assign -n VolunteeringApp --json', { ensureExitCode: 0, }).jsonOutput; - expect(output.result).to.have.all.keys(['successes', 'failures']); - expect(output.result.successes).to.have.length(1); - expect(output.result.successes[0]).to.have.all.keys(['name', 'value']); - expect(output.result.failures).to.have.length(0); + expect(output?.result).to.have.all.keys(['successes', 'failures']); + expect(output?.result.successes).to.have.length(1); + expect(output?.result.successes[0]).to.have.all.keys(['name', 'value']); + expect(output?.result.failures).to.have.length(0); }); it('creates a secondary user', () => { - const output = execCmd('force:user:create --json -a Other', { ensureExitCode: 0 }).jsonOutput; + const output = execCmd('user:create --json -a Other', { ensureExitCode: 0 }).jsonOutput; - expect(output.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); - expect(output.result.fields).to.have.all.keys( + expect(output?.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); + expect(output?.result.fields).to.have.all.keys( 'id', 'username', 'alias', // because it's set in the command @@ -84,24 +85,21 @@ describe('verifies all commands run successfully ', () => { 'lastname', 'timezonesidkey' ); - expect(output.result.fields.id).to.not.equal(mainUserId); + expect(output?.result.fields.id).to.not.equal(mainUserId); }); it('assigns permset to the secondary user', () => { - const output = execCmd( - 'force:user:permset:assign -n VolunteeringApp --json --onbehalfof Other', - { - ensureExitCode: 0, - } - ).jsonOutput; - expect(output.result).to.have.all.keys(['successes', 'failures']); - expect(output.result.successes).to.have.length(1); - expect(output.result.successes[0]).to.have.all.keys(['name', 'value']); - expect(output.result.failures).to.have.length(0); + const output = execCmd('user:permset:assign -n VolunteeringApp --json --onbehalfof Other', { + ensureExitCode: 0, + }).jsonOutput; + expect(output?.result).to.have.all.keys(['successes', 'failures']); + expect(output?.result.successes).to.have.length(1); + expect(output?.result.successes[0]).to.have.all.keys(['name', 'value']); + expect(output?.result.failures).to.have.length(0); }); it('lists the users', () => { - const result = execCmd('force:user:list --json', { ensureExitCode: 0 }).jsonOutput.result; + const result = execCmd('user:list --json', { ensureExitCode: 0 }).jsonOutput?.result; expect(result).to.be.an('array').with.length(2); expect(result).each.have.all.keys([ 'defaultMarker', @@ -117,85 +115,83 @@ describe('verifies all commands run successfully ', () => { }); it('generates new passwords for main user', () => { - const output = execCmd('force:user:password:generate --json', { ensureExitCode: 0 }).jsonOutput; + const output = execCmd('user:password:generate --json', { ensureExitCode: 0 }).jsonOutput; expect(output).to.have.property('result').includes.keys(['username', 'password']); }); it('generates new passwords for main user testing default length and complexity', () => { - const output = execCmd<{ username: string; password: string }>('force:user:password:generate --json', { + const output = execCmd<{ username: string; password: string }>('user:password:generate --json', { ensureExitCode: 0, - }).jsonOutput.result; + }).jsonOutput?.result; // testing default length - expect(output.password.length).to.equal(13); + expect(output?.password?.length).to.equal(13); const complexity5Regex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$|%^&*()[]_-])'); // testing default complexity - expect(complexity5Regex.test(output.password)); + expect(complexity5Regex.test(output?.password ?? '')).to.be.true; }); it('generates new passwords for main user testing length 11 and complexity 5', () => { - const output = execCmd<{ username: string; password: string }>('force:user:password:generate --json -l 11 -c 3', { + const output = execCmd<{ username: string; password: string }>('user:password:generate --json -l 11 -c 3', { ensureExitCode: 0, - }).jsonOutput.result; - expect(output.password.length).to.equal(11); + }).jsonOutput?.result; + expect(output?.password.length).to.equal(11); const complexity3Regex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'); - expect(complexity3Regex.test(output.password)); + expect(complexity3Regex.test(output?.password ?? '')); }); it('generates new password for secondary user (onbehalfof)', () => { - const output = execCmd('force:user:password:generate -o Other --json', { ensureExitCode: 0 }).jsonOutput; + const output = execCmd('user:password:generate -o Other --json', { ensureExitCode: 0 }).jsonOutput; expect(output).to.have.property('result').includes.keys(['username', 'password']); }); it('generates new password for secondary user (onbehalfof) with length 12', () => { - const output = execCmd<{ username: string; password: string }>( - 'force:user:password:generate -o Other --json -l 12', - { ensureExitCode: 0 } - ).jsonOutput.result; + const output = execCmd<{ username: string; password: string }>('user:password:generate -o Other --json -l 12', { + ensureExitCode: 0, + }).jsonOutput?.result; - expect(output.password.length).to.equal(12); + expect(output?.password.length).to.equal(12); const complexity5Regex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$|%^&*()[]_-])'); // testing the default complexity - expect(complexity5Regex.test(output.password)); + expect(complexity5Regex.test(output?.password ?? '')); }); it('generates new password for secondary user (onbehalfof) with complexity 3', () => { - const output = execCmd<{ username: string; password: string }>( - 'force:user:password:generate -o Other --json -c 3', - { ensureExitCode: 0 } - ).jsonOutput.result; + const output = execCmd<{ username: string; password: string }>('user:password:generate -o Other --json -c 3', { + ensureExitCode: 0, + }).jsonOutput?.result; // testing default length - expect(output.password.length).to.equal(13); + expect(output?.password.length).to.equal(13); const complexity3Regex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'); - expect(complexity3Regex.test(output.password)); + expect(complexity3Regex.test(output?.password ?? '')); }); it('generates new password for secondary user (onbehalfof) with complexity 7 should thrown an error', () => { - const output = execCmd('force:user:password:generate -o Other --json -c 7', { ensureExitCode: 1 }).jsonOutput; - expect(output.message).to.equal('Expected integer less than or equal to 5 but received 7'); + const output = execCmd('user:password:generate -o Other --json -c 7', { ensureExitCode: 1 }).jsonOutput; + expect(output?.message).to.equal('Expected integer less than or equal to 5 but received 7'); }); it('generates new password for secondary user (onbehalfof) with length 7 should thrown an error', () => { - const output = execCmd('force:user:password:generate -o Other --json -l 7', { ensureExitCode: 1 }).jsonOutput; - expect(output.message).to.equal('Expected integer greater than or equal to 8 but received 7'); + const output = execCmd('user:password:generate -o Other --json -l 7', { ensureExitCode: 1 }).jsonOutput; + expect(output?.message).to.equal('Expected integer greater than or equal to 8 but received 7'); }); it('assigns 2 permsets to the main user', () => { - const output = execCmd('force:user:permset:assign -n PS2,PS3 --json', { + const output = execCmd('user:permset:assign -n PS2,PS3 --json', { ensureExitCode: 0, }).jsonOutput; - expect(output.result).to.have.all.keys(['successes', 'failures']); - expect(output.result.successes).to.have.length(2); + expect(output?.result).to.have.all.keys(['successes', 'failures']); + expect(output?.result.successes).to.have.length(2); - expect(output.result.successes[0]).to.have.all.keys(['name', 'value']); - expect(output.result.failures).to.have.length(0); + expect(output?.result.successes[0]).to.have.all.keys(['name', 'value']); + expect(output?.result.failures).to.have.length(0); }); it('assigns 2 permsets to the secondary user', () => { - const output = execCmd('force:user:permset:assign -n PS2,PS3 -o Other --json', { + const output = execCmd('user:permset:assign -n PS2,PS3 -o Other --json', { ensureExitCode: 0, }).jsonOutput; - expect(output.result).to.have.all.keys(['successes', 'failures']); - expect(output.result.successes).to.have.length(2); - expect(output.result.successes[0]).to.have.all.keys(['name', 'value']); - expect(output.result.failures).to.have.length(0); + expect(output?.result).to.have.all.keys(['successes', 'failures']); + expect(output?.result.successes).to.have.length(2); + expect(output?.result.successes[0]).to.have.all.keys(['name', 'value']); + expect(output?.result.failures).to.have.length(0); }); after(async () => { diff --git a/test/allCommandsNoJSON.nut.ts b/test/allCommandsNoJSON.nut.ts index 4ea5e9ae..12fa8a5e 100644 --- a/test/allCommandsNoJSON.nut.ts +++ b/test/allCommandsNoJSON.nut.ts @@ -29,45 +29,45 @@ describe('verifies all commands run successfully (no json)', () => { ], }); - execCmd('force:source:push', { cli: 'sfdx' }); + execCmd('source:push', { cli: 'sfdx' }); }); it('user display', () => { - execCmd('force:user:display', { ensureExitCode: 0 }); + execCmd('user:display', { ensureExitCode: 0 }); }); it('assigns a permset to the default user', () => { - execCmd('force:user:permset:assign -n VolunteeringApp', { ensureExitCode: 0 }); + execCmd('user:permset:assign -n VolunteeringApp', { ensureExitCode: 0 }); }); it('creates a secondary user', () => { - execCmd('force:user:create -a Other', { ensureExitCode: 0 }); + execCmd('user:create -a Other', { ensureExitCode: 0 }); }); it('assigns permset to the secondary user', () => { - execCmd('force:user:permset:assign -n VolunteeringApp --onbehalfof Other', { + execCmd('user:permset:assign -n VolunteeringApp --onbehalfof Other', { ensureExitCode: 0, }); }); it('lists the users', () => { - execCmd('force:user:list', { ensureExitCode: 0 }); + execCmd('user:list', { ensureExitCode: 0 }); }); it('generates new passwords for main user', () => { - execCmd('force:user:password:generate', { ensureExitCode: 0 }); + execCmd('user:password:generate', { ensureExitCode: 0 }); }); it('generates new password for secondary user (onbehalfof)', () => { - execCmd('force:user:password:generate -o Other', { ensureExitCode: 0 }); + execCmd('user:password:generate -o Other', { ensureExitCode: 0 }); }); it('assigns 2 permsets to the main user', () => { - execCmd('force:user:permset:assign -n PS2,PS3', { ensureExitCode: 0 }); + execCmd('user:permset:assign -n PS2,PS3', { ensureExitCode: 0 }); }); it('assigns 2 permsets to the secondary user', () => { - execCmd('force:user:permset:assign -n PS2,PS3 -o Other', { ensureExitCode: 0 }); + execCmd('user:permset:assign -n PS2,PS3 -o Other', { ensureExitCode: 0 }); }); after(async () => { diff --git a/test/commands/user/create.nut.ts b/test/commands/create.nut.ts similarity index 63% rename from test/commands/user/create.nut.ts rename to test/commands/create.nut.ts index 8b7d3f3e..6b4eea5e 100644 --- a/test/commands/user/create.nut.ts +++ b/test/commands/create.nut.ts @@ -7,9 +7,10 @@ import * as path from 'path'; import { expect } from 'chai'; -import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { AuthInfo, Connection } from '@salesforce/core'; -import { UserCreateOutput } from '../../../src/commands/force/user/create'; +import { UserCreateOutput } from '../../src/commands/user/create'; +import { AuthList } from '../../src/commands/user/list'; let session: TestSession; @@ -31,20 +32,19 @@ describe('creates a user from a file and verifies', () => { }, ], }); - - execCmd('force:source:push', { cli: 'sfdx' }); + execCmd('source:push', { cli: 'sfdx' }); }); it('creates a user with setuniqueusername from username on commandline', () => { const testUsername = 'test@test.test'; const output = execCmd( - `force:user:create --json --setuniqueusername username=${testUsername} profileName="Chatter Free User"`, + `user:create --json --setuniqueusername username=${testUsername} profileName="Chatter Free User"`, { ensureExitCode: 0, } ).jsonOutput; - expect(output.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); - const usernameResult = output.result.fields.username as string; + expect(output?.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); + const usernameResult = output?.result.fields.username as string; expect(usernameResult.startsWith(testUsername)).to.be.true; // ends with . followed by the prefix for orgId (lowercased!) and 15 more lowercase characters or digits expect(usernameResult).matches(/.*\.00d[a-z|\d]{15}$/); @@ -52,30 +52,30 @@ describe('creates a user from a file and verifies', () => { it('creates a user with setuniqueusername from username on commandline', () => { const output = execCmd( - `force:user:create --json -f ${path.join('config', 'fileWithUsername.json')} --setuniqueusername`, + `user:create --json -f ${path.join('config', 'fileWithUsername.json')} --setuniqueusername`, { ensureExitCode: 0, } ).jsonOutput; - expect(output.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); - const usernameResult = output.result.fields.username as string; + expect(output?.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); + const usernameResult = output?.result.fields.username as string; expect(usernameResult).matches(/not\.unique@test\.test\.00d[a-z|\d]{15}$/); }); it('creates a secondary user with password and permsets assigned', () => { const output = execCmd( - `force:user:create --json -a Other -f ${path.join('config', 'complexUser.json')}`, + `user:create --json -a Other -f ${path.join('config', 'complexUser.json')}`, { ensureExitCode: 0, } ).jsonOutput; - expect(output.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); - expect(output.result.permissionSetAssignments).to.deep.equal(['VolunteeringApp']); - createdUserId = output.result.fields.id as string; + expect(output?.result).to.have.all.keys(['orgId', 'permissionSetAssignments', 'fields']); + expect(output?.result.permissionSetAssignments).to.deep.equal(['VolunteeringApp']); + createdUserId = output?.result.fields.id as string; }); it('verifies the permission set assignment in the org', async () => { const connection = await Connection.create({ - authInfo: await AuthInfo.create({ username: session.orgs.get('default').username }), + authInfo: await AuthInfo.create({ username: session.orgs.get('default')?.username }), }); const queryResult = await connection.query<{ Id: string; PermissionSet: { Name: string } }>( `select PermissionSet.Name from PermissionSetAssignment where AssigneeId = '${createdUserId}'` @@ -83,6 +83,12 @@ describe('creates a user from a file and verifies', () => { // there will also be a profile in there, too, with a cryptic name on it expect(queryResult.records.some((assignment) => assignment.PermissionSet.Name === 'VolunteeringApp')); }); + it('verifies the new user appears in list of users for the org', async () => { + const output = execCmd(`user:list --json -u ${session.orgs.get('default')?.username}`, { + ensureExitCode: 0, + }).jsonOutput; + expect(output?.result.find((user) => user.userId === createdUserId)).to.be.ok; + }); after(async () => { await session.zip(undefined, 'artifacts'); diff --git a/test/commands/create.test.ts b/test/commands/create.test.ts new file mode 100644 index 00000000..39416b84 --- /dev/null +++ b/test/commands/create.test.ts @@ -0,0 +1,452 @@ +/* + * Copyright (c) 2020, 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 + */ + +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import * as fs from 'fs'; +import { AuthInfo, Connection, DefaultUserFields, Logger, User, Org } from '@salesforce/core'; +import { Config } from '@oclif/core'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; +import { expect } from 'chai'; +import UserCreateCommand from '../../src/commands/user/create'; + +const username = 'defaultusername@test.com'; +const originalUserId = '0052D0000043PawWWR'; +const newUserId = '0052D0000044PawWWR'; + +describe('user:create', () => { + const $$ = new TestContext(); + + const testOrg = new MockTestOrgData(); + const devHub = new MockTestOrgData(); + const newUser = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.aliases = ['mydevhub']; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; + + it('will properly merge fields regardless of capitalization', async () => { + // notice the varied capitalization + $$.SANDBOX.stub(fs.promises, 'readFile').resolves( + JSON.stringify({ + id: originalUserId, + Username: '1605130295132_test-j6asqt5qoprs@example.com', + Alias: 'testAlias', + Email: username, + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + localeSidKey: 'en_US', + ProfileId: '00e2D000000bNexWWR', + LastName: 'User', + timeZoneSidKey: 'America/Los_Angeles', + permsets: ['permset1', 'permset2'], + }) + ); + + const createCommand = new UserCreateCommand(['-f', 'userConfig.json'], {} as Config); + + // @ts-ignore + createCommand.flags = { 'definition-file': 'testing' }; + + // @ts-ignore private method + const res = await createCommand.aggregateFields({ + id: '00555000006lCspAAE', + emailEncodingKey: 'UTF-8', + languageLocaleKey: 'en_US', + localeSidKey: 'en_US', + profileId: '00e55000000fvDdAAI', + lastName: 'User', + timeZoneSidKey: 'America/Los_Angeles', + }); + expect(res).to.deep.equal({ + alias: 'testAlias', + email: 'defaultusername@test.com', + emailEncodingKey: 'UTF-8', + id: '0052D0000043PawWWR', + languageLocaleKey: 'en_US', + lastName: 'User', + permsets: ['permset1', 'permset2'], + localeSidKey: 'en_US', + profileId: '00e2D000000bNexWWR', + timeZoneSidKey: 'America/Los_Angeles', + username: '1605130295132_test-j6asqt5qoprs@example.com', + }); + }); + + async function prepareStubs(throws: { license?: boolean; duplicate?: boolean } = {}, readsFile?) { + await $$.stubAuths(testOrg, devHub); + $$.stubUsers({ [testOrg.username]: [] }); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': testOrg.username }); + $$.stubAliases({ testAlias: '1605130295132_test-j6asqt5qoprs@example.com' }); + + const userInfo = { + id: newUserId, + username: '1605130295132_test-j6asqt5qoprs@example.com', + alias: 'testAlias', + email: username, + emailEncodingKey: 'UTF-8', + languageLocaleKey: 'en_US', + localeSidKey: 'en_US', + profileId: '00e2D000000bNexWWR', + profileName: 'profileFromArgs', + lastName: 'User', + timeZoneSidKey: 'America/Los_Angeles', + }; + // @ts-ignore + $$.SANDBOX.stub(DefaultUserFields, 'create').callsFake(() => { + const defaultFields = new DefaultUserFields({ templateUser: userInfo.username }); + // @ts-ignore + defaultFields.userFields = userInfo; + return Promise.resolve(defaultFields); + }); + $$.SANDBOX.stub(AuthInfo.prototype, 'getFields').returns({ + userId: newUserId, + username: '1605130295132_test-j6asqt5qoprs@example.com', + alias: 'testAlias', + }); + $$.SANDBOX.stub(Org.prototype, 'getOrgId').returns(testOrg.orgId); + + if (throws.license) { + $$.SANDBOX.stub(User.prototype, 'createUser').throws(new Error('LICENSE_LIMIT_EXCEEDED')); + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'query').resolves({ + records: [{ Name: 'testName' }], + done: true, + totalSize: 1, + }); + } else if (throws.duplicate) { + $$.SANDBOX.stub(User.prototype, 'createUser').throws(new Error('DUPLICATE_USERNAME')); + } else { + $$.SANDBOX.stub(User.prototype, 'createUser').callsFake(async (): Promise => { + const authInfo = await AuthInfo.create({ username: newUser.username }); + newUser.orgId = (await Org.create({ aliasOrUsername: testOrg.username })).getOrgId(); + return authInfo.save(newUser); + }); + } + + if (readsFile) { + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'query').resolves({ + records: [{ Id: '12345678' }], + done: true, + totalSize: 1, + }); + $$.SANDBOX.stub(Logger.prototype, 'debug'); + } + } + + it('will handle a merge multiple permsets and profilenames from args and file (permsets from args)', async () => { + await prepareStubs({}, { profilename: 'profileFromFile', permsets: ['perm1', 'perm2'] }); + $$.SANDBOX.stub(User.prototype, 'assignPassword').resolves(); + const expected = { + orgId: testOrg.orgId, + permissionSetAssignments: ['permCLI', 'permCLI2'], + fields: { + alias: 'testAlias', + email: 'defaultusername@test.com', + emailencodingkey: 'UTF-8', + id: newUserId, + 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 createCommand = new UserCreateCommand( + [ + '--json', + '--target-org', + testOrg.username, + '--target-dev-hub', + 'devhub@test.com', + "permsets='permCLI, permCLI2'", + 'generatepassword=true', + ], + {} as Config + ); + const result = await createCommand.run(); + expect(result).to.deep.equal(expected); + }); + + // test + // .do(async () => { + // await prepareStubs({}, {permsets: ['perm1', 'perm2']}); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // '--definitionfile', + // 'tempfile.json', + // 'profilename=profileFromArgs', + // 'username=user@cliArgs.com', + // ]) + // .it('will handle a merge multiple permsets and profilenames from args and file (permsets from file)', (ctx) => { + // const expected = { + // orgId: 'abc123', + // permissionSetAssignments: ['perm1', 'perm2'], + // fields: { + // alias: 'testAlias', + // email: 'defaultusername@test.com', + // emailencodingkey: 'UTF-8', + // id: newUserId, + // 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); + // }); + // + // test + // .do(async () => { + // await prepareStubs({}, false); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // ]) + // .it('default create creates user exactly from DefaultUserFields', (ctx) => { + // const expected = { + // orgId: 'abc123', + // permissionSetAssignments: [], + // fields: { + // alias: 'testAlias', + // email: username, + // emailencodingkey: 'UTF-8', + // id: newUserId, + // 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); + // }); + // + // test + // .do(async () => { + // await prepareStubs({}, {generatepassword: true, permsets: ['test1', 'test2']}); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--definitionfile', + // 'parent/child/file.json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // 'email=me@my.org', + // 'generatepassword=false', + // ]) + // // 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 = { + // orgId: 'abc123', + // permissionSetAssignments: ['test1', 'test2'], + // fields: { + // alias: 'testAlias', + // email: 'me@my.org', + // emailencodingkey: 'UTF-8', + // id: newUserId, + // 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); + // }); + // + // test + // .do(async () => { + // await prepareStubs({}, {generatepassword: true, profilename: 'System Administrator'}); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--definitionfile', + // 'parent/child/file.json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // 'email=me@my.org', + // 'generatepassword=false', + // "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' + // .it( + // 'will merge fields from the cli args, and the definitionfile correctly, preferring cli args, cli args > file > default', + // (ctx) => { + // const expected = { + // orgId: 'abc123', + // permissionSetAssignments: [], + // fields: { + // alias: 'testAlias', + // email: 'me@my.org', + // emailencodingkey: 'UTF-8', + // id: newUserId, + // 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); + // } + // ); + // + // test + // .do(async () => { + // await prepareStubs({license: true}, false); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // ]) + // .it('will handle a failed `createUser` call with a licenseLimitExceeded error', (ctx) => { + // const result = JSON.parse(ctx.stdout); + // expect(result.status).to.equal(1); + // expect(result.message).to.equal('There are no available user licenses for the user profile "testName".'); + // expect(result.name).to.equal('licenseLimitExceeded'); + // }); + // + // test + // .do(async () => { + // await prepareStubs({duplicate: true}, false); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // ]) + // .it('will handle a failed `createUser` call with a DuplicateUsername error', (ctx) => { + // const result = JSON.parse(ctx.stdout); + // expect(result.status).to.equal(1); + // expect(result.name).to.equal('duplicateUsername'); + // expect(result.message).to.equal( + // 'The username "1605130295132_test-j6asqt5qoprs@example.com" already exists in this or another Salesforce org. Usernames must be unique across all Salesforce orgs.' + // ); + // }); + // + // test + // .do(async () => { + // await prepareStubs({}, false); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // 'username=user@cliFlag.com', + // '--setuniqueusername', + // ]) + // .it('will append the org id to the passed username if the setuniqueusername is used', (ctx) => { + // const expected = { + // orgId: 'abc123', + // permissionSetAssignments: [], + // fields: { + // alias: 'testAlias', + // email: 'defaultusername@test.com', + // emailencodingkey: 'UTF-8', + // id: newUserId, + // languagelocalekey: 'en_US', + // lastname: 'User', + // localesidkey: 'en_US', + // profileid: '00e2D000000bNexWWR', + // timezonesidkey: 'America/Los_Angeles', + // username: 'user@cliFlag.com.abc123', + // }, + // }; + // const result = JSON.parse(ctx.stdout).result; + // expect(result).to.deep.equal(expected); + // }); + // + // test + // .do(async () => { + // await prepareStubs({}, false); + // }) + // .stdout() + // .command([ + // 'user:create', + // '--json', + // '--targetusername', + // 'testUser1@test.com', + // '--targetdevhubusername', + // 'devhub@test.com', + // '--setuniqueusername', + // ]) + // .it( + // 'will not append the org id to the username if the setuniqueusername is used but username was generated by the CLI', + // (ctx) => { + // const expected = { + // orgId: 'abc123', + // permissionSetAssignments: [], + // fields: { + // alias: 'testAlias', + // email: 'defaultusername@test.com', + // emailencodingkey: 'UTF-8', + // id: newUserId, + // 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); + // } + // ); +}); diff --git a/test/commands/display.test.ts b/test/commands/display.test.ts new file mode 100644 index 00000000..c8cc5eb1 --- /dev/null +++ b/test/commands/display.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020, 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 { AuthInfo, Connection, Org } from '@salesforce/core'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; +import { expect } from 'chai'; +import { Config } from '@oclif/core'; +import { UserDisplayCommand } from '../../src/commands/user/display'; + +const username = 'defaultusername@test.com'; + +describe('user:display', () => { + const $$ = new TestContext(); + + const testOrg = new MockTestOrgData(); + const devHub = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; + const defaultOrg = new MockTestOrgData(); + defaultOrg.orgId = 'abc123'; + defaultOrg.username = 'defaultusername@test.com'; + defaultOrg.userId = '1234567890'; + defaultOrg.instanceUrl = 'instanceURL'; + defaultOrg.loginUrl = 'login.test.com'; + defaultOrg.password = '-a098u234/1!@#'; + + async function prepareStubs(queries = false) { + await $$.stubAuths(testOrg, devHub, defaultOrg); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': defaultOrg.username }); + $$.stubAliases({ testAlias: 'defaultusername@test.com' }); + + if (queries) { + $$.SANDBOX.stub(Org.prototype, 'readUserAuthFiles').callsFake( + (): Promise => + Promise.resolve([ + { + getFields: () => ({ + username: 'defaultusername@test.com', + instanceUrl: 'instanceURL', + loginUrl: 'login.test.com', + }), + } as AuthInfo, + ]) + ); + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'query') + .withArgs(`SELECT name FROM Profile WHERE Id IN (SELECT ProfileId FROM User WHERE username='${username}')`) + .resolves({ + records: [{ Name: 'QueriedName' }], + done: true, + totalSize: 1, + }) + .withArgs(`SELECT id FROM User WHERE username='${username}'`) + .resolves({ + records: [{ Id: 'QueriedId' }], + done: true, + totalSize: 1, + }); + } else { + $$.SANDBOX.stub(Org.prototype, 'readUserAuthFiles').callsFake( + (): Promise => + Promise.resolve([ + { + getFields: () => ({ + username: 'defaultusername@test.com', + userProfileName: 'profileName', + userId: '1234567890', + instanceUrl: 'instanceURL', + loginUrl: 'login.test.com', + password: '-a098u234/1!@#', + }), + } as AuthInfo, + ]) + ); + } + } + + it('should display the correct information from the default user', async () => { + await prepareStubs(); + // testUser1@test.com is aliased to testUser + const expected = { + accessToken: defaultOrg.accessToken, + alias: 'testAlias', + id: '1234567890', + instanceUrl: 'instanceURL', + loginUrl: 'login.test.com', + orgId: 'abc123', + password: '-a098u234/1!@#', + profileName: 'profileName', + username: 'defaultusername@test.com', + }; + const displayCommand = new UserDisplayCommand( + ['--json', '--target-org', defaultOrg.username, '--target-dev-hub', devHub.username], + {} as Config + ); + const result = await displayCommand.run(); + expect(result).to.deep.equal(expected); + }); + + it('should make queries to the server to get userId and profileName', async () => { + await prepareStubs(true); + // testUser1@test.com is aliased to testUser + const expected = { + accessToken: defaultOrg.accessToken, + alias: 'testAlias', + id: 'QueriedId', + instanceUrl: 'instanceURL', + loginUrl: 'login.test.com', + orgId: 'abc123', + profileName: 'QueriedName', + username: defaultOrg.username, + }; + const displayCommand = new UserDisplayCommand( + ['--json', '--targetusername', 'defaultusername@test.com', '--targetdevhubusername', devHub.username], + {} as Config + ); + const result = await displayCommand.run(); + expect(result).to.deep.equal(expected); + }); +}); diff --git a/test/commands/user/list.test.ts b/test/commands/list.test.ts similarity index 54% rename from test/commands/user/list.test.ts rename to test/commands/list.test.ts index fd70fb15..abbc7ce5 100644 --- a/test/commands/user/list.test.ts +++ b/test/commands/list.test.ts @@ -5,9 +5,11 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { $$, expect, test } from '@salesforce/command/lib/test'; import { Connection, Org } from '@salesforce/core'; -import { stubMethod } from '@salesforce/ts-sinon'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; +import { Config } from '@oclif/core'; +import { expect } from 'chai'; +import { UserListCommand } from '../../src/commands/user/list'; const user1 = 'defaultusername@test.com'; const user2 = 'otherUser@test.com'; @@ -36,11 +38,40 @@ const expected = [ }, ]; -describe('force:user:list', () => { +describe('user:list', () => { + const $$ = new TestContext(); + + const user1Org = new MockTestOrgData(); + user1Org.username = user1; + user1Org.orgId = 'abc123'; + user1Org.aliases = ['testAlias']; + user1Org.accessToken = 'accessToken'; + user1Org.instanceUrl = 'instanceURL'; + user1Org.loginUrl = 'login.test.com'; + user1Org.userId = '0052D0000043PbGQAU'; + + const user2Org = new MockTestOrgData(); + user2Org.username = user2; + user2Org.orgId = 'abc123'; + user2Org.aliases = []; + user2Org.accessToken = 'accessToken'; + user2Org.instanceUrl = 'instanceURL'; + user2Org.loginUrl = 'login.test.com'; + user2Org.userId = '0052D0000043PcBQAU'; + + const devHub = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; + beforeEach(async () => { - stubMethod($$.SANDBOX, Org, 'create').resolves(Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getConnection').returns(Connection.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'readUserAuthFiles').returns([ + await $$.stubAuths(user1Org, devHub); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': user1Org.username }); + $$.stubAliases({ testAlias: user1 }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + $$.SANDBOX.stub(Org.prototype, 'readUserAuthFiles').returns([ { getUsername: () => user1, getFields: () => ({ @@ -64,10 +95,7 @@ describe('force:user:list', () => { }), }, ]); - stubMethod($$.SANDBOX, Org.prototype, 'getOrgId').returns('abc123'); - stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns(user1); - $$.stubAliases({ testAlias: user1 }); - stubMethod($$.SANDBOX, Connection.prototype, 'query') + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'query') .withArgs('SELECT username, profileid, id FROM User') .resolves({ records: [ @@ -87,6 +115,8 @@ describe('force:user:list', () => { Id: '0052D0000043PbGQAU', }, ], + done: true, + totalSize: 3, }) .withArgs('SELECT id, name FROM Profile') .resolves({ @@ -100,22 +130,26 @@ describe('force:user:list', () => { Name: 'Analytics Cloud Integration User', }, ], + done: true, + totalSize: 2, }); }); - test - .stdout() - .command(['force:user:list', '--json', '--targetusername', 'testUser', '--targetdevhubusername', 'devhub@test.com']) - .it('should display the correct information invoked with an alias', (ctx) => { - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - }); + it('should display the correct information invoked with an alias', async () => { + const listComm = new UserListCommand( + ['--json', '--target-org', 'testAlias', '--target-dev-hub', devHub.username], + {} as Config + ); + const result = await listComm.run(); + expect(result).to.deep.equal(expected); + }); - test - .stdout() - .command(['force:user:list', '--json', '--targetusername', user1, '--targetdevhubusername', 'devhub@test.com']) - .it('should display the correct information invoked by name', (ctx) => { - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - }); + it('should display the correct information invoked by name', async () => { + const listComm = new UserListCommand( + ['--json', '--target-org', user1, '--target-dev-hub', devHub.username], + {} as Config + ); + const result = await listComm.run(); + expect(result).to.deep.equal(expected); + }); }); diff --git a/test/commands/user/create.test.ts b/test/commands/user/create.test.ts deleted file mode 100644 index 8aa2c821..00000000 --- a/test/commands/user/create.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Copyright (c) 2020, 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 - */ - -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import { $$, expect, test } from '@salesforce/command/lib/test'; -import * as fse from 'fs-extra'; -import { AuthInfo, Connection, DefaultUserFields, Logger, Org, User, UserFields } from '@salesforce/core'; -import { stubMethod } from '@salesforce/ts-sinon'; -import { Config } from '@oclif/core'; -import UserCreateCommand from '../../../src/commands/force/user/create'; - -const username = 'defaultusername@test.com'; -const originalUserId = '0052D0000043PawWWR'; -const newUserId = '0052D0000044PawWWR'; - -describe('force:user:create', () => { - it('will properly merge fields regardless of capitalization', async () => { - // notice the varied capitalization - stubMethod($$.SANDBOX, fse, 'readJson').resolves({ - id: originalUserId, - Username: '1605130295132_test-j6asqt5qoprs@example.com', - Alias: 'testAlias', - Email: username, - EmailEncodingKey: 'UTF-8', - LanguageLocaleKey: 'en_US', - localeSidKey: 'en_US', - ProfileId: '00e2D000000bNexWWR', - LastName: 'User', - timeZoneSidKey: 'America/Los_Angeles', - permsets: ['permset1', 'permset2'], - }); - - const createCommand = new UserCreateCommand(['-f', 'userConfig.json'], {} as Config); - - // @ts-ignore - createCommand.flags = { definitionfile: 'testing' }; - - // @ts-ignore private method - const res = await createCommand.aggregateFields({ - id: '00555000006lCspAAE', - emailEncodingKey: 'UTF-8', - languageLocaleKey: 'en_US', - localeSidKey: 'en_US', - profileId: '00e55000000fvDdAAI', - lastName: 'User', - timeZoneSidKey: 'America/Los_Angeles', - }); - expect(res).to.deep.equal({ - alias: 'testAlias', - email: 'defaultusername@test.com', - emailEncodingKey: 'UTF-8', - id: '0052D0000043PawWWR', - languageLocaleKey: 'en_US', - lastName: 'User', - permsets: ['permset1', 'permset2'], - localeSidKey: 'en_US', - profileId: '00e2D000000bNexWWR', - timeZoneSidKey: 'America/Los_Angeles', - username: '1605130295132_test-j6asqt5qoprs@example.com', - }); - }); - - async function prepareStubs(throws: { license?: boolean; duplicate?: boolean } = {}, readsFile?) { - stubMethod($$.SANDBOX, Org, 'create').resolves(Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getConnection').returns(Connection.prototype); - stubMethod($$.SANDBOX, DefaultUserFields, 'create').resolves({ - getFields: (): UserFields => ({ - id: originalUserId, - username: '1605130295132_test-j6asqt5qoprs@example.com', - alias: 'testAlias', - email: username, - emailEncodingKey: 'UTF-8', - languageLocaleKey: 'en_US', - localeSidKey: 'en_US', - profileId: '00e2D000000bNexWWR', - lastName: 'User', - timeZoneSidKey: 'America/Los_Angeles', - }), - }); - - 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'); - stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); - stubMethod($$.SANDBOX, AuthInfo.prototype, 'getFields').returns({ - userId: newUserId, - username: '1605130295132_test-j6asqt5qoprs@example.com', - alias: 'testAlias', - email: username, - emailEncodingKey: 'UTF-8', - languageLocaleKey: 'en_US', - localeSidKey: 'en_US', - profileId: '00e2D000000bNexWWR', - lastName: 'User', - timeZoneSidKey: 'America/Los_Angeles', - }); - if (throws.license) { - stubMethod($$.SANDBOX, User.prototype, 'createUser').throws(new Error('LICENSE_LIMIT_EXCEEDED')); - stubMethod($$.SANDBOX, Connection.prototype, 'query').resolves({ records: [{ Name: 'testName' }] }); - } else if (throws.duplicate) { - stubMethod($$.SANDBOX, User.prototype, 'createUser').throws(new Error('DUPLICATE_USERNAME')); - } else { - stubMethod($$.SANDBOX, User.prototype, 'createUser').resolves(AuthInfo.prototype); - } - - if (readsFile) { - stubMethod($$.SANDBOX, Connection.prototype, 'query').resolves({ - records: [{ Id: '12345678' }], - }); - stubMethod($$.SANDBOX, Logger.prototype, 'debug'); - stubMethod($$.SANDBOX, fse, 'readJson').resolves(readsFile); - } - - $$.stubAliases({ testAlias: '1605130295132_test-j6asqt5qoprs@example.com' }); - } - test - .do(async () => { - stubMethod($$.SANDBOX, User.prototype, 'assignPassword').resolves(); - await prepareStubs({}, { profilename: 'profileFromFile', permsets: ['perm1', 'perm2'] }); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - "permsets='permCLI, permCLI2'", - 'generatepassword=true', - 'profilename=profileFromArgs', - ]) - .it('will handle a merge multiple permsets and profilenames from args and file (permsets from args)', (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: ['permCLI', 'permCLI2'], - fields: { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - }); - - test - .do(async () => { - await prepareStubs({}, { permsets: ['perm1', 'perm2'] }); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - '--definitionfile', - 'tempfile.json', - 'profilename=profileFromArgs', - 'username=user@cliArgs.com', - ]) - .it('will handle a merge multiple permsets and profilenames from args and file (permsets from file)', (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: ['perm1', 'perm2'], - fields: { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - }); - - test - .do(async () => { - await prepareStubs({}, false); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('default create creates user exactly from DefaultUserFields', (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: [], - fields: { - alias: 'testAlias', - email: username, - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - }); - - test - .do(async () => { - await prepareStubs({}, { generatepassword: true, permsets: ['test1', 'test2'] }); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--definitionfile', - 'parent/child/file.json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - 'email=me@my.org', - 'generatepassword=false', - ]) - // 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 = { - orgId: 'abc123', - permissionSetAssignments: ['test1', 'test2'], - fields: { - alias: 'testAlias', - email: 'me@my.org', - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - }); - - test - .do(async () => { - await prepareStubs({}, { generatepassword: true, profilename: 'System Administrator' }); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--definitionfile', - 'parent/child/file.json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - 'email=me@my.org', - 'generatepassword=false', - "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' - .it( - 'will merge fields from the cli args, and the definitionfile correctly, preferring cli args, cli args > file > default', - (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: [], - fields: { - alias: 'testAlias', - email: 'me@my.org', - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - } - ); - - test - .do(async () => { - await prepareStubs({ license: true }, false); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('will handle a failed `createUser` call with a licenseLimitExceeded error', (ctx) => { - const result = JSON.parse(ctx.stdout); - expect(result.status).to.equal(1); - expect(result.message).to.equal('There are no available user licenses for the user profile "testName".'); - expect(result.name).to.equal('licenseLimitExceeded'); - }); - - test - .do(async () => { - await prepareStubs({ duplicate: true }, false); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('will handle a failed `createUser` call with a DuplicateUsername error', (ctx) => { - const result = JSON.parse(ctx.stdout); - expect(result.status).to.equal(1); - expect(result.name).to.equal('duplicateUsername'); - expect(result.message).to.equal( - 'The username "1605130295132_test-j6asqt5qoprs@example.com" already exists in this or another Salesforce org. Usernames must be unique across all Salesforce orgs.' - ); - }); - - test - .do(async () => { - await prepareStubs({}, false); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - 'username=user@cliFlag.com', - '--setuniqueusername', - ]) - .it('will append the org id to the passed username if the setuniqueusername is used', (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: [], - fields: { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailencodingkey: 'UTF-8', - id: newUserId, - languagelocalekey: 'en_US', - lastname: 'User', - localesidkey: 'en_US', - profileid: '00e2D000000bNexWWR', - timezonesidkey: 'America/Los_Angeles', - username: 'user@cliFlag.com.abc123', - }, - }; - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - }); - - test - .do(async () => { - await prepareStubs({}, false); - }) - .stdout() - .command([ - 'force:user:create', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - '--setuniqueusername', - ]) - .it( - 'will not append the org id to the username if the setuniqueusername is used but username was generated by the CLI', - (ctx) => { - const expected = { - orgId: 'abc123', - permissionSetAssignments: [], - fields: { - alias: 'testAlias', - email: 'defaultusername@test.com', - emailencodingkey: 'UTF-8', - id: newUserId, - 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); - } - ); -}); diff --git a/test/commands/user/display.test.ts b/test/commands/user/display.test.ts deleted file mode 100644 index 603e9adf..00000000 --- a/test/commands/user/display.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2020, 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, test } from '@salesforce/command/lib/test'; -import { Connection, Org } from '@salesforce/core'; -import { stubMethod } from '@salesforce/ts-sinon'; - -const username = 'defaultusername@test.com'; - -describe('force:user:display', () => { - async function prepareStubs(queries = false) { - stubMethod($$.SANDBOX, Org, 'create').resolves(Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getConnection').returns(Connection.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns(username); - stubMethod($$.SANDBOX, Org.prototype, 'getOrgId').returns('abc123'); - if (queries) { - stubMethod($$.SANDBOX, Org.prototype, 'readUserAuthFiles').returns([ - { - getFields: () => ({ - username: 'defaultusername@test.com', - instanceUrl: 'instanceURL', - loginUrl: 'login.test.com', - }), - }, - ]); - stubMethod($$.SANDBOX, Connection.prototype, 'query') - .withArgs(`SELECT name FROM Profile WHERE Id IN (SELECT ProfileId FROM User WHERE username='${username}')`) - .resolves({ - records: [{ Name: 'QueriedName' }], - }) - .withArgs(`SELECT id FROM User WHERE username='${username}'`) - .resolves({ records: [{ Id: 'QueriedId' }] }); - } else { - stubMethod($$.SANDBOX, Org.prototype, 'readUserAuthFiles').returns([ - { - getFields: () => ({ - username: 'defaultusername@test.com', - userProfileName: 'profileName', - userId: '1234567890', - instanceUrl: 'instanceURL', - loginUrl: 'login.test.com', - password: '-a098u234/1!@#', - }), - }, - ]); - } - - $$.stubAliases({ testAlias: 'defaultusername@test.com' }); - } - - test - .do(async () => { - await prepareStubs(); - }) - .stdout() - .command([ - 'force:user:display', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('should display the correct information from the default user', (ctx) => { - // testUser1@test.com is aliased to testUser - const expected = { - alias: 'testAlias', - id: '1234567890', - instanceUrl: 'instanceURL', - loginUrl: 'login.test.com', - orgId: 'abc123', - password: '-a098u234/1!@#', - profileName: 'profileName', - username: 'defaultusername@test.com', - }; - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - }); - - test - .do(async () => { - await prepareStubs(true); - }) - .stdout() - .command([ - 'force:user:display', - '--json', - '--targetusername', - 'testUser1@test.com', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('should make queries to the server to get userId and profileName', (ctx) => { - // testUser1@test.com is aliased to testUser - const expected = { - alias: 'testAlias', - id: 'QueriedId', - instanceUrl: 'instanceURL', - loginUrl: 'login.test.com', - orgId: 'abc123', - profileName: 'QueriedName', - username: 'defaultusername@test.com', - }; - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - }); - - test - .stdout() - .do(async () => { - await prepareStubs(); - }) - .command([ - 'force:user:display', - '--json', - '--targetusername', - '00D54000000GxYk!ARwAQOZEBxYvKxKlNJfvJEGyj7fNj6TA61Fn5RxJzaYm79hR9IYjx2x147a2GH2DGtne21DW.g_8DD0rzNF.COIAXcmq0FfJ', - '--targetdevhubusername', - 'devhub@test.com', - ]) - .it('should throw when username is an accessToken', (ctx) => { - const response = JSON.parse(ctx.stdout); - expect(response.status).to.equal(1); - expect(response.name).to.equal('accessTokenError'); - }); -}); diff --git a/test/commands/user/password/generate.test.ts b/test/commands/user/password/generate.test.ts index 309d39d7..9f0a430a 100644 --- a/test/commands/user/password/generate.test.ts +++ b/test/commands/user/password/generate.test.ts @@ -5,103 +5,195 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { $$, expect, test } from '@salesforce/command/lib/test'; -import { AuthInfo, Connection, Org, User, Messages } from '@salesforce/core'; -import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; -import { MockTestOrgData } from '@salesforce/core/lib/testSetup'; +import { Connection, Messages, Org, User } from '@salesforce/core'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; import { SecureBuffer } from '@salesforce/core/lib/crypto/secureBuffer'; +import { expect } from 'chai'; +import { Config } from '@oclif/core'; +import { PasswordData, UserPasswordGenerateCommand } from '../../../../src/commands/user/password/generate'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-user', 'password.generate'); -describe('force:user:password:generate', () => { - let authInfoStub: StubbedType; - const testData = new MockTestOrgData(); +describe('user:password:generate', () => { + const $$ = new TestContext(); + + class UserPasswordGenerateCommandTest extends UserPasswordGenerateCommand { + public constructor(argv: string[], config: Config) { + super(argv, config); + } + + public async run(): Promise { + return super.run(); + } + } + + const testOrg = new MockTestOrgData(); + const devHub = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; let queryStub: sinon.SinonStub; async function prepareStubs(throws = false, generatePassword = true) { - const authFields = await testData.getConfig(); - authInfoStub = stubInterface($$.SANDBOX, { getFields: () => authFields }); - stubMethod($$.SANDBOX, AuthInfo, 'create').callsFake(async () => authInfoStub); - stubMethod($$.SANDBOX, Connection, 'create').callsFake(async () => Connection.prototype); - stubMethod($$.SANDBOX, Org, 'create').callsFake(async () => Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns('defaultusername@test.com'); - stubMethod($$.SANDBOX, Org.prototype, 'retrieveMaxApiVersion').returns('51.0'); - stubMethod($$.SANDBOX, User, 'create').callsFake(async () => User.prototype); - queryStub = stubMethod($$.SANDBOX, Connection.prototype, 'singleRecordQuery').resolves({ + await $$.stubAuths(testOrg, devHub); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': testOrg.username }); + queryStub = $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'singleRecordQuery').resolves({ Id: '0052D0000043PawWWR', }); - + $$.SANDBOXES.ORGS.stub(Org.prototype, 'retrieveMaxApiVersion').resolves('51.0'); if (generatePassword) { const secureBuffer: SecureBuffer = new SecureBuffer(); secureBuffer.consume(Buffer.from('abc', 'utf8')); - stubMethod($$.SANDBOX, User, 'generatePasswordUtf8').returns(secureBuffer); + $$.SANDBOX.stub(User, 'generatePasswordUtf8').returns(secureBuffer); } if (throws) { - stubMethod($$.SANDBOX, User.prototype, 'assignPassword').throws(new Error('Cannot set password for self')); + $$.SANDBOX.stub(User.prototype, 'assignPassword').throws(new Error('Cannot set password for self')); } else { - stubMethod($$.SANDBOX, User.prototype, 'assignPassword').resolves(); + $$.SANDBOX.stub(User.prototype, 'assignPassword').resolves(); } $$.stubAliases({ testAlias: 'testUser1@test.com' }); } - test - .do(async () => { - await prepareStubs(); - }) - .stdout() - .command(['force:user:password:generate', '--json', '--onbehalfof', 'testUser1@test.com, testUser2@test.com']) - .it('should generate a new password for the user', (ctx) => { - const expected = [ - { - username: 'testUser1@test.com', - password: 'abc', - }, - { - username: 'testUser2@test.com', - password: 'abc', - }, - ]; - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - expect(authInfoStub.update.callCount).to.equal(2); - expect(queryStub.callCount).to.equal(2); - }); - - test - .do(() => prepareStubs()) - .stdout() - .command(['force:user:password:generate', '--json']) - .it('should generate a new password for the default user', (ctx) => { - const expected = { username: 'defaultusername@test.com', password: 'abc' }; - const result = JSON.parse(ctx.stdout).result; - expect(result).to.deep.equal(expected); - expect(authInfoStub.update.callCount).to.equal(1); - expect(queryStub.callCount).to.equal(1); - }); - - test - .do(() => prepareStubs(false, false)) - .stdout() - .command(['force:user:password:generate', '--json', '-u', 'testUser2@test.com', '-l', '12']) - .it('should generate a new passsword of length 12', (ctx) => { - const result = JSON.parse(ctx.stdout).result; - expect(result.password.length).to.deep.equal(12); - }); - - test - .do(async () => prepareStubs(true)) - - .stdout() - .command(['force:user:password:generate', '--json']) - .it('should throw the correct error with warning message', (ctx) => { - const result = JSON.parse(ctx.stdout); + it('should generate a new password for the user', async () => { + await prepareStubs(); + const expected = [ + { + username: 'testUser1@test.com', + password: 'abc', + }, + { + username: 'testUser2@test.com', + password: 'abc', + }, + ]; + const passwordGenerate = new UserPasswordGenerateCommandTest( + ['--target-org', testOrg.username, '--json', '--on-behalf-of', 'testUser1@test.com', 'testUser2@test.com'], + {} as Config + ); + const result = await passwordGenerate.run(); + expect(result).to.deep.equal(expected); + expect(queryStub.callCount).to.equal(2); + }); + it('should generate a new password for the default user', async () => { + await prepareStubs(); + const expected = { username: testOrg.username, password: 'abc' }; + const passwordGenerate = new UserPasswordGenerateCommandTest(['--json'], {} as Config); + const result = await passwordGenerate.run(); + expect(result).to.deep.equal(expected); + expect(queryStub.callCount).to.equal(1); + }); + it('should generate a new passsword of length 12', async () => { + await prepareStubs(false, false); + const passwordGenerate = new UserPasswordGenerateCommandTest( + ['--target-org', testOrg.username, '-l', '12', '--json'], + {} as Config + ); + const result = (await passwordGenerate.run()) as PasswordData; + expect(result.password.length).to.equal(12); + }); + it('should throw the correct error with warning message', async () => { + await prepareStubs(true); + const passwordGenerate = new UserPasswordGenerateCommandTest(['--json'], {} as Config); + try { + await passwordGenerate.run(); + expect.fail('should have thrown an error'); + } catch (result) { expect(result.message).to.equal(messages.getMessage('noSelfSetError')); - expect(result.status).to.equal(1); - expect(result.name).to.equal('noSelfSetError'); - expect(authInfoStub.update.callCount).to.equal(0); - expect(queryStub.callCount).to.equal(1); - }); + expect(result.name).to.equal('NoSelfSetError'); + } + }); }); +// describe('user:password:generate', () => { +// let authInfoStub: StubbedType; +// const testData = new MockTestOrgData(); +// let queryStub: sinon.SinonStub; +// +// async function prepareStubs(throws = false, generatePassword = true) { +// const authFields = await testData.getConfig(); +// authInfoStub = stubInterface($$.SANDBOX, {getFields: () => authFields}); +// stubMethod($$.SANDBOX, AuthInfo, 'create').callsFake(async () => authInfoStub); +// stubMethod($$.SANDBOX, Connection, 'create').callsFake(async () => Connection.prototype); +// stubMethod($$.SANDBOX, Org, 'create').callsFake(async () => Org.prototype); +// stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns('defaultusername@test.com'); +// stubMethod($$.SANDBOX, Org.prototype, 'retrieveMaxApiVersion').returns('51.0'); +// stubMethod($$.SANDBOX, User, 'create').callsFake(async () => User.prototype); +// queryStub = stubMethod($$.SANDBOX, Connection.prototype, 'singleRecordQuery').resolves({ +// Id: '0052D0000043PawWWR', +// }); +// +// if (generatePassword) { +// const secureBuffer: SecureBuffer = new SecureBuffer(); +// secureBuffer.consume(Buffer.from('abc', 'utf8')); +// stubMethod($$.SANDBOX, User, 'generatePasswordUtf8').returns(secureBuffer); +// } +// +// if (throws) { +// stubMethod($$.SANDBOX, User.prototype, 'assignPassword').throws(new Error('Cannot set password for self')); +// } else { +// stubMethod($$.SANDBOX, User.prototype, 'assignPassword').resolves(); +// } +// +// $$.stubAliases({testAlias: 'testUser1@test.com'}); +// } +// +// test +// .do(async () => { +// await prepareStubs(); +// }) +// .stdout() +// .command(['user:password:generate', '--json', '--onbehalfof', 'testUser1@test.com, testUser2@test.com']) +// .it('should generate a new password for the user', (ctx) => { +// const expected = [ +// { +// username: 'testUser1@test.com', +// password: 'abc', +// }, +// { +// username: 'testUser2@test.com', +// password: 'abc', +// }, +// ]; +// const result = JSON.parse(ctx.stdout).result; +// expect(result).to.deep.equal(expected); +// expect(authInfoStub.update.callCount).to.equal(2); +// expect(queryStub.callCount).to.equal(2); +// }); +// +// test +// .do(() => prepareStubs()) +// .stdout() +// .command(['user:password:generate', '--json']) +// .it('should generate a new password for the default user', (ctx) => { +// const expected = {username: 'defaultusername@test.com', password: 'abc'}; +// const result = JSON.parse(ctx.stdout).result; +// expect(result).to.deep.equal(expected); +// expect(authInfoStub.update.callCount).to.equal(1); +// expect(queryStub.callCount).to.equal(1); +// }); +// +// test +// .do(() => prepareStubs(false, false)) +// .stdout() +// .command(['user:password:generate', '--json', '-u', 'testUser2@test.com', '-l', '12']) +// .it('should generate a new passsword of length 12', (ctx) => { +// const result = JSON.parse(ctx.stdout).result; +// expect(result.password.length).to.deep.equal(12); +// }); +// +// test +// .do(async () => prepareStubs(true)) +// +// .stdout() +// .command(['user:password:generate', '--json']) +// .it('should throw the correct error with warning message', (ctx) => { +// const result = JSON.parse(ctx.stdout); +// expect(result.message).to.equal(messages.getMessage('noSelfSetError')); +// expect(result.status).to.equal(1); +// expect(result.name).to.equal('noSelfSetError'); +// expect(authInfoStub.update.callCount).to.equal(0); +// expect(queryStub.callCount).to.equal(1); +// }); +// }); diff --git a/test/commands/user/permset/assign.test.ts b/test/commands/user/permset/assign.test.ts index bbcc0e7b..181737e2 100644 --- a/test/commands/user/permset/assign.test.ts +++ b/test/commands/user/permset/assign.test.ts @@ -5,79 +5,100 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { $$, expect, test } from '@salesforce/command/lib/test'; -import { Connection, Org, User } from '@salesforce/core'; -import { stubMethod } from '@salesforce/ts-sinon'; +import { Connection, User } from '@salesforce/core'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; +import { expect } from 'chai'; +import { Config } from '@oclif/core'; +import { UserPermSetAssignCommand } from '../../../../src/commands/user/permset/assign'; +import { PermsetAssignResult } from '../../../../src/baseCommands/user/permset/assign'; + +describe('user:permset:assign', () => { + const $$ = new TestContext(); + + class UserPermSetAssignCommandTest extends UserPermSetAssignCommand { + public constructor(argv: string[], config: Config) { + super(argv, config); + } + + public async run(): Promise { + return super.run(); + } + } + + const testOrg = new MockTestOrgData(); + testOrg.username = 'defaultusername@test.com'; + const devHub = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; -describe('force:user:permset:assign', () => { async function prepareStubs(throws = false) { - stubMethod($$.SANDBOX, Org.prototype, 'getConnection').returns(Connection.prototype); - stubMethod($$.SANDBOX, Connection.prototype, 'query').resolves({ records: [{ Id: 1234567890 }] }); - stubMethod($$.SANDBOX, Org, 'create').callsFake(async () => Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns('defaultusername@test.com'); - stubMethod($$.SANDBOX, User, 'create').callsFake(async () => User.prototype); + await $$.stubAuths(testOrg, devHub); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': testOrg.username }); + + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'query').resolves({ + records: [{ Id: '1234567890' }], + done: true, + totalSize: 1, + }); + if (throws) { - stubMethod($$.SANDBOX, User.prototype, 'assignPermissionSets').throws( + $$.SANDBOX.stub(User.prototype, 'assignPermissionSets').throws( new Error('Permission set "abc" not found in target org. Do you need to push source?') ); } else { - stubMethod($$.SANDBOX, User.prototype, 'assignPermissionSets').resolves(); + $$.SANDBOX.stub(User.prototype, 'assignPermissionSets').resolves(); } $$.stubAliases({ testAlias: 'testUser2@test.com' }); } - test - .do(async () => { - await prepareStubs(); - }) - .stdout() - .command([ - 'force:user:permset:assign', - '--json', - '--onbehalfof', - 'testAlias, testUser2@test.com', - '--permsetname', - 'DreamHouse, LargeDreamHouse', - ]) - .it('should assign both permsets to both users', (ctx) => { - // testUser1@test.com is aliased to testUser - const expected = [ - { - name: 'testAlias', - value: 'DreamHouse', - }, - { - name: 'testAlias', - value: 'LargeDreamHouse', - }, - { - name: 'testUser2@test.com', - value: 'DreamHouse', - }, - { - name: 'testUser2@test.com', - value: 'LargeDreamHouse', - }, - ]; - const result = JSON.parse(ctx.stdout).result; - expect(result.successes).to.deep.equal(expected); - }); - - test - .do(async () => { - await prepareStubs(true); - }) - .stdout() - .command(['force:user:permset:assign', '--json', '--permsetname', 'PERM2']) - .it('should fail with the correct error message', (ctx) => { - // testUser1@test.com is aliased to testUser - const expected = [ - { - name: 'defaultusername@test.com', - message: 'Permission set "abc" not found in target org. Do you need to push source?', - }, - ]; - const result = JSON.parse(ctx.stdout).result; - expect(result.failures).to.deep.equal(expected); - }); + it('should assign both permsets to both users', async () => { + await prepareStubs(); + // testUser1@test.com is aliased to testUser + const expected = [ + { + name: 'testAlias', + value: 'DreamHouse', + }, + { + name: 'testAlias', + value: 'LargeDreamHouse', + }, + { + name: 'testUser2@test.com', + value: 'DreamHouse', + }, + { + name: 'testUser2@test.com', + value: 'LargeDreamHouse', + }, + ]; + const userPermSetAssign = new UserPermSetAssignCommandTest( + [ + '--json', + '--on-behalf-of', + 'testAlias', + 'testUser2@test.com', + '--perm-set-name', + 'DreamHouse', + 'LargeDreamHouse', + ], + {} as Config + ); + const result = await userPermSetAssign.run(); + expect(result.successes).to.deep.equal(expected); + }); + it('should fail with the correct error message', async () => { + await prepareStubs(true); + // testUser1@test.com is aliased to testUser + const expected = [ + { + name: 'defaultusername@test.com', + message: 'Permission set "abc" not found in target org. Do you need to push source?', + }, + ]; + const userPermSetAssign = new UserPermSetAssignCommandTest(['--json', '--permsetname', 'PERM2'], {} as Config); + const result = await userPermSetAssign.run(); + expect(result.failures).to.deep.equal(expected); + }); }); diff --git a/test/commands/user/permsetlicense/assign.nut.ts b/test/commands/user/permsetlicense/assign.nut.ts index ef7c0605..ea3a5b19 100644 --- a/test/commands/user/permsetlicense/assign.nut.ts +++ b/test/commands/user/permsetlicense/assign.nut.ts @@ -7,8 +7,8 @@ import * as path from 'path'; import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; -import { PSLResult } from '../../../../src/commands/force/user/permsetlicense/assign'; -import { UserCreateOutput } from '../../../../src/commands/force/user/create'; +import { PSLResult } from '../../../../src/baseCommands/user/permsetlicense/assign'; +import { UserCreateOutput } from '../../../../src/commands/user/create'; describe('PermissionSetLicense tests', () => { const testPSL = 'IdentityConnect'; @@ -30,20 +30,20 @@ describe('PermissionSetLicense tests', () => { ], }); - execCmd('force:source:push', { cli: 'sfdx' }); + execCmd('source:push', { cli: 'sfdx' }); }); it('assigns a psl to default user', () => { - const commandResult = execCmd(`force:user:permsetlicense:assign -n ${testPSL} --json`, { + const commandResult = execCmd(`user:permsetlicense:assign -n ${testPSL} --json`, { ensureExitCode: 0, - }).jsonOutput.result; - expect(commandResult.failures).to.be.an('array').with.length(0); - expect(commandResult.successes.every((success) => success.value === testPSL)).to.be.true; + }).jsonOutput?.result; + expect(commandResult?.failures).to.be.an('array').with.length(0); + expect(commandResult?.successes.every((success) => success.value === testPSL)).to.be.true; expect(commandResult).to.not.have.property('warnings'); }); it('assigns a psl to default user successfully if already assigned', () => { - const commandResult = execCmd(`force:user:permsetlicense:assign -n ${testPSL} --json`, { + const commandResult = execCmd(`user:permsetlicense:assign -n ${testPSL} --json`, { ensureExitCode: 0, }).jsonOutput as { status: number; result: PSLResult; warnings: string[] }; expect(commandResult.result.failures).to.be.an('array').with.length(0); @@ -54,7 +54,7 @@ describe('PermissionSetLicense tests', () => { it('fails for non-existing psl', () => { const badPSL = 'badPSL'; - execCmd(`force:user:permsetlicense:assign -n ${badPSL} --json`, { + execCmd(`user:permsetlicense:assign -n ${badPSL} --json`, { ensureExitCode: 1, }); }); @@ -62,25 +62,25 @@ describe('PermissionSetLicense tests', () => { describe('multiple PSL via onBehalfOf', () => { it('assigns a psl to multiple users via onBehalfOf', async () => { const anotherPSL = 'SurveyCreatorPsl'; - const originalUsername = session.orgs.get('default').username; + const originalUsername = session.orgs.get('default')?.username; expect(originalUsername).to.be.a('string'); // create a second user const secondUsername = execCmd( - `force:user:create --json -a Other -f ${path.join('config', 'fullUser.json')}`, + `user:create --json -a Other -f ${path.join('config', 'fullUser.json')}`, { ensureExitCode: 0, } - ).jsonOutput.result.fields.username as string; + ).jsonOutput?.result.fields.username as string; expect(secondUsername).to.be.a('string'); const commandResult = execCmd( - `force:user:permsetlicense:assign -n ${anotherPSL} -o ${originalUsername},Other --json`, + `user:permsetlicense:assign -n ${anotherPSL} -o ${originalUsername},Other --json`, { ensureExitCode: 0, } - ).jsonOutput.result; - expect(commandResult.failures).to.deep.equal([]); - expect(commandResult.successes).to.deep.equal([ + ).jsonOutput?.result; + expect(commandResult?.failures).to.deep.equal([]); + expect(commandResult?.successes).to.deep.equal([ { value: anotherPSL, name: originalUsername }, { value: anotherPSL, name: secondUsername }, ]); @@ -89,26 +89,26 @@ describe('PermissionSetLicense tests', () => { it('assigns a psl to multiple users via onBehalfOf (partial success)', async () => { // sales console user can't be assigned to a platform license const anotherPSL = 'SalesConsoleUser'; - const originalUsername = session.orgs.get('default').username; + const originalUsername = session.orgs.get('default')?.username; const secondUsername = execCmd( - `force:user:create --json -f ${path.join('config', 'chatterUser.json')}`, + `user:create --json -f ${path.join('config', 'chatterUser.json')}`, { ensureExitCode: 0, } - ).jsonOutput.result.fields.username as string; + ).jsonOutput?.result.fields.username as string; const commandResult = execCmd( - `force:user:permsetlicense:assign -n ${anotherPSL} -o ${originalUsername},${secondUsername} --json`, + `user:permsetlicense:assign -n ${anotherPSL} -o ${originalUsername},${secondUsername} --json`, { ensureExitCode: 68, } - ).jsonOutput.result; + ).jsonOutput?.result; - expect(commandResult.failures).to.have.length(1); - expect(commandResult.failures[0].name).to.equal(secondUsername); - expect(commandResult.failures[0].message).to.include("user license doesn't support it"); - expect(commandResult.successes).to.deep.equal([{ value: anotherPSL, name: originalUsername }]); + expect(commandResult?.failures).to.have.length(1); + expect(commandResult?.failures[0].name).to.equal(secondUsername); + expect(commandResult?.failures[0].message).to.include("user license doesn't support it"); + expect(commandResult?.successes).to.deep.equal([{ value: anotherPSL, name: originalUsername }]); }); }); diff --git a/test/commands/user/permsetlicense/assign.test.ts b/test/commands/user/permsetlicense/assign.test.ts index 93f0d44c..52aa1211 100644 --- a/test/commands/user/permsetlicense/assign.test.ts +++ b/test/commands/user/permsetlicense/assign.test.ts @@ -4,33 +4,47 @@ * 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, test } from '@salesforce/command/lib/test'; -import { AuthInfo, Connection, Org } from '@salesforce/core'; -import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; -import { MockTestOrgData } from '@salesforce/core/lib/testSetup'; +import { Connection } from '@salesforce/core'; +// import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; +import { Config } from '@oclif/core'; +import { expect } from 'chai'; +import { UserPermSetLicenseAssignCommand } from '../../../../src/commands/user/permsetlicense/assign'; +import { PSLResult } from '../../../../src/baseCommands/user/permsetlicense/assign'; -describe('force:user:permsetlicense:assign', () => { - let authInfoStub: StubbedType; - const testData = new MockTestOrgData(); +describe('user:permsetlicense:assign', () => { + const $$ = new TestContext(); + + class UserPermSetLicenseAssignCommandTest extends UserPermSetLicenseAssignCommand { + public constructor(argv: string[], config: Config) { + super(argv, config); + } + + public async run(): Promise { + return super.run(); + } + } + + const testOrg = new MockTestOrgData(); + testOrg.username = 'defaultusername@test.com'; + const devHub = new MockTestOrgData(); + devHub.username = 'mydevhub.org'; + devHub.devHubUsername = 'mydevhub.org'; + devHub.isDevHub = true; const goodPSL = 'existingPSL'; const badPSL = 'nonExistingPSL'; - const defaultUsername = 'defaultusername@test.com'; const username1 = 'testUser1@test.com'; const username2 = 'testUser2@test.com'; - async function prepareStubs() { - const authFields = await testData.getConfig(); - authInfoStub = stubInterface($$.SANDBOX, { getFields: () => authFields }); - stubMethod($$.SANDBOX, AuthInfo, 'create').callsFake(async () => authInfoStub); - stubMethod($$.SANDBOX, Org, 'create').callsFake(async () => Org.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getConnection').returns(Connection.prototype); - stubMethod($$.SANDBOX, Org.prototype, 'getUsername').returns(defaultUsername); + async function prepareStubs() { + await $$.stubAuths(testOrg, devHub); + await $$.stubConfig({ 'target-dev-hub': devHub.username, 'target-org': testOrg.username }); $$.stubAliases({ testAlias: username1 }); - stubMethod($$.SANDBOX, Connection.prototype, 'singleRecordQuery') + $$.SANDBOXES.CONNECTION.stub(Connection.prototype, 'singleRecordQuery') // matcher for all user queries .withArgs(`select Id from User where Username = '${username1}'`) .resolves({ Id: '0051234567890123' }) @@ -43,52 +57,46 @@ describe('force:user:permsetlicense:assign', () => { .withArgs(`select Id from PermissionSetLicense where DeveloperName = '${badPSL}' or MasterLabel = '${badPSL}'`) .throws(); - stubMethod($$.SANDBOX, Connection.prototype, 'sobject').callsFake(() => ({ + $$.SANDBOX.stub(Connection.prototype, 'sobject').callsFake(() => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore create() { return Promise.resolve({ success: true }); }, })); } - test - .do(async () => { - await prepareStubs(); - }) - .stdout() - .command([ - 'force:user:permsetlicense:assign', - '--json', - '--onbehalfof', - [username1, username2].join(','), - '--name', - goodPSL, - ]) - .it('should assign the one permset to both users', (ctx) => { - const expected = [ - { - name: username1, - value: goodPSL, - }, - { - name: username2, - value: goodPSL, - }, - ]; - - const result = JSON.parse(ctx.stdout).result; - expect(result.failures).to.deep.equal([]); - expect(result.successes).to.deep.equal(expected); - }); + it('should assign the one permset to both users', async () => { + await prepareStubs(); + const expected = [ + { + name: username1, + value: goodPSL, + }, + { + name: username2, + value: goodPSL, + }, + ]; + const userPermSetLicenseAssignCommand = new UserPermSetLicenseAssignCommandTest( + ['--json', '--onbehalfof', [username1, username2].join(','), '--name', goodPSL], + {} as Config + ); + const result = await userPermSetLicenseAssignCommand.run(); + expect(result.failures).to.deep.equal([]); + expect(result.successes).to.deep.equal(expected); + }); - test - .do(async () => { - await prepareStubs(); - }) - .stdout() - .command(['force:user:permsetlicense:assign', '--json', '--name', badPSL]) - .it('should fail with the correct error message when no PSL exists', (ctx) => { - const result = JSON.parse(ctx.stdout); - expect(result.message).to.equal('PermissionSetLicense not found'); - expect(result.status).to.equal(1); - }); + it('should fail with the correct error message when no PSL exists', async () => { + await prepareStubs(); + const userPermSetLicenseAssignCommand = new UserPermSetLicenseAssignCommandTest( + ['--json', '--name', badPSL], + {} as Config + ); + try { + await userPermSetLicenseAssignCommand.run(); + } catch (e) { + expect(e.message).to.equal('PermissionSetLicense not found'); + } + }); }); diff --git a/test/df17AppBuilding/orgInit.sh b/test/df17AppBuilding/orgInit.sh index b7b005e1..e248a027 100755 --- a/test/df17AppBuilding/orgInit.sh +++ b/test/df17AppBuilding/orgInit.sh @@ -1,7 +1,7 @@ sfdx shane:org:create -f config/project-scratch-def.json -s -d 1 --userprefix blitz --userdomain back.log -sfdx force:source:push -sfdx force:user:permset:assign -n VolunteeringApp -sfdx force:data:tree:import -p data/masterImportPlan.json -sfdx force:apex:execute -f scripts/setup.cls +sfdx source:push +sfdx user:permset:assign -n VolunteeringApp +sfdx data:tree:import -p data/masterImportPlan.json +sfdx apex:execute -f scripts/setup.cls sfdx shane:user:password:set -p sfdx1234 -g User -l User -sfdx force:org:open +sfdx org:open diff --git a/test/tsconfig.json b/test/tsconfig.json index 3fee52bd..c13783c5 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@salesforce/dev-config/tsconfig-test", "include": ["./**/*.ts"], "compilerOptions": { + "strictNullChecks": true, "skipLibCheck": true } } diff --git a/tsconfig.json b/tsconfig.json index 12618d12..0ca4eaac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@salesforce/dev-config/tsconfig", "compilerOptions": { + "strictNullChecks": true, "outDir": "lib", "rootDir": "src" }, diff --git a/yarn.lock b/yarn.lock index 6da8a27a..da397456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -729,7 +729,7 @@ is-wsl "^2.1.1" tslib "^2.3.1" -"@oclif/core@^1.20.3", "@oclif/core@^1.20.4", "@oclif/core@^1.21.0", "@oclif/core@^1.22.0": +"@oclif/core@^1.20.3", "@oclif/core@^1.20.4", "@oclif/core@^1.21.0", "@oclif/core@^1.22.0", "@oclif/core@^1.6.3": version "1.22.0" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.22.0.tgz#dfdd76db6435cc1be2de7bbe25c23302332b9297" integrity sha512-Bvyi6uFbmpkFl9XUATsGMlqEDGfqMKWL0Mu5VQTuPg7/NIyfygYkaburn11uGkOp0a8yG6fPpyVBfGmztjNPGA== @@ -893,14 +893,6 @@ resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-3.0.3.tgz#e679ad10535e31d333f809f7a71335cc9aef1e55" integrity sha512-KX8gMYA9ujBPOd1HFsV9e0iEx7Uoj8AG/3YsW4TtWQTg4lJvr82qNm7o/cFQfYRIt+jw7Ew/4oL4A22zOT+IRA== -"@oclif/test@^2.2.12": - version "2.2.12" - resolved "https://registry.yarnpkg.com/@oclif/test/-/test-2.2.12.tgz#bb97a8c678b349e0e323013be67d68f619e6aaf2" - integrity sha512-6s1XwvBTXHdVjVZY/qDgMl74NVvoy8MQoknqT/YfR9K3P/6fPW4xeZqemtvrvU4heM5kzSShta5sk0I28MXHMg== - dependencies: - "@oclif/core" "^1.20.4" - fancy-test "^2.0.7" - "@octokit/auth-token@^2.4.4": version "2.5.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" @@ -1029,22 +1021,10 @@ strip-ansi "6.0.1" ts-retry-promise "^0.7.0" -"@salesforce/command@^5.2.28": - version "5.2.28" - resolved "https://registry.yarnpkg.com/@salesforce/command/-/command-5.2.28.tgz#90ddfdbace98b3b2732ebf1ef90fcb57e095b72d" - integrity sha512-mx4Gzg/6w5jjmAhKsgfMiKTESfY0wHtnmnAJlnNNPLfypG4jO6VO9s3C8OrouhsnQ6VBHGTwXVeF9Pu3mIlKYg== - dependencies: - "@oclif/core" "^1.20.4" - "@oclif/test" "^2.2.12" - "@salesforce/core" "^3.32.6" - "@salesforce/kit" "^1.8.0" - "@salesforce/ts-types" "^1.7.1" - chalk "^2.4.2" - -"@salesforce/core@^3.31.17", "@salesforce/core@^3.32.6": - version "3.32.6" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.32.6.tgz#ee42663ffbf9db2478247fa7e6af3df652eebb5f" - integrity sha512-+j9nXnVnK4H+YmRfhcmSW/RZu2XIM6zKlXImsy+XhWBV1zOvM1JZiQngj3pdrGck7hoMc68YfWP0UEVzmM/k+g== +"@salesforce/core@^3.31.17", "@salesforce/core@^3.32.11", "@salesforce/core@^3.32.9", "@salesforce/core@^3.8.0": + version "3.32.11" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.32.11.tgz#d40740ae4f3d3f835a77395d91b29fd7f6bdc2c6" + integrity sha512-fAxDcu2N+hw1IMWsvpp1aRxeczY7C6Yb0idaYY0npNzDmC+4wGdWkZlPoz6FHhidgBfAGnwRKVSr7/UL/o8mDQ== dependencies: "@salesforce/bunyan" "^2.0.0" "@salesforce/kit" "^1.8.0" @@ -1052,7 +1032,7 @@ "@salesforce/ts-types" "^1.5.21" "@types/graceful-fs" "^4.1.5" "@types/semver" "^7.3.13" - ajv "^8.11.0" + ajv "^8.11.2" archiver "^5.3.0" change-case "^4.1.2" debug "^3.2.7" @@ -1108,7 +1088,7 @@ typedoc-plugin-missing-exports "0.23.0" typescript "^4.1.3" -"@salesforce/kit@^1.7.1", "@salesforce/kit@^1.8.0": +"@salesforce/kit@^1.5.17", "@salesforce/kit@^1.7.1", "@salesforce/kit@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@salesforce/kit/-/kit-1.8.0.tgz#d5b8d83d5b0b866cb76840dc7a18e115589d86a0" integrity sha512-Pr9CWAIzVYKZRWvM76lyhEtF3CPmVdIfgbqRD7KT/YZdbLstX3KHYBxCyx3TyWZr5qROv96n+jRIBiIFI9LGGw== @@ -1117,18 +1097,21 @@ shx "^0.3.3" tslib "^2.2.0" -"@salesforce/plugin-command-reference@^1.5.6": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@salesforce/plugin-command-reference/-/plugin-command-reference-1.5.6.tgz#598028e6f3fb816ce50080365020fe122a55219b" - integrity sha512-cB50cCoWQRV8WI382BOoefVE8UdAuuRTScWcnw8P9eF0aSpt60PnXRivxaPuiIcwVbxZC9HDemXdA2+1aDgAew== +"@salesforce/plugin-command-reference@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@salesforce/plugin-command-reference/-/plugin-command-reference-2.2.8.tgz#b75571df46ad5210ce5170cb42de57cd6965ff84" + integrity sha512-vyJzYDOGDmF3NyVhdRwLNkM+lVj3pzwbusHq4eGJDsxrlgWALeg0IVbEeEI+qUk32BK/wpwcJNbPRTc4Bem36w== dependencies: - "@salesforce/command" "^5.2.28" - "@salesforce/core" "^3.32.6" - "@types/lodash.uniqby" "^4.7.7" + "@oclif/core" "^1.6.3" + "@salesforce/core" "^3.8.0" + "@salesforce/kit" "^1.5.17" + "@salesforce/sf-plugins-core" "^1.9.0" + "@salesforce/ts-types" "^1.5.20" chalk "^3.0.0" - handlebars "4.7.7" + fs-extra "^10.0.1" + handlebars "^4.7.7" lodash.uniqby "^4.7.0" - shelljs "^0.8.5" + mkdirp "^1.0.4" tslib "^2" "@salesforce/prettier-config@^0.0.2": @@ -1141,6 +1124,18 @@ resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.4.0.tgz#7dff427c8059895d8108176047aee600703c82d6" integrity sha512-BJ25uphssN42Zy6kksheFHMTLiR98AAHe/Wxnv0T4dYxtrEbUjSXVAGKZqfewJPFXA4xB5gxC+rQZtfz6xKCFg== +"@salesforce/sf-plugins-core@^1.21.3", "@salesforce/sf-plugins-core@^1.9.0": + version "1.21.3" + resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-1.21.3.tgz#f2ecc6e8a6c0b59ff15bb59d81408199983e18aa" + integrity sha512-Q5E+BLORRA92Em0IDuRESe/2qc/zrarJA7uRhDvLEaqic4V8rM8q9jVy7lddaOyqo1DYPbYx9Ck7YST3g3k3kQ== + dependencies: + "@oclif/core" "^1.22.0" + "@salesforce/core" "^3.32.9" + "@salesforce/kit" "^1.7.1" + "@salesforce/ts-types" "^1.7.1" + chalk "^4" + inquirer "^8.2.5" + "@salesforce/ts-sinon@1.4.2": version "1.4.2" resolved "https://registry.yarnpkg.com/@salesforce/ts-sinon/-/ts-sinon-1.4.2.tgz#7b76f80c104c891334b84ad664ab048fd1fbb1ff" @@ -1150,7 +1145,7 @@ sinon "^5.1.1" tslib "^2.2.0" -"@salesforce/ts-types@^1.5.21", "@salesforce/ts-types@^1.7.1": +"@salesforce/ts-types@^1.5.20", "@salesforce/ts-types@^1.5.21", "@salesforce/ts-types@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-1.7.1.tgz#86b0d0c3bfd5c9b1752662a019a3d2f3bc8ff118" integrity sha512-jwZb8fYxbOmEckoJTxG2+5ZEJNJOFxmRJ/FioPnSu4IMFzpK3QbyujfqpHwLfPKHq0xlKRMx+F8QAVVKI/PA4w== @@ -1259,7 +1254,7 @@ resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.23.tgz#00ad332d847f2e3961d7bba4d1886dd767e1f92e" integrity sha512-qYKP8sIM7VVLuDb5BkRBoHy28OHZWrUhPTO7WgpErhVVM9wnzmMi/Jgg8SyfMy6oheBjO0QiwWbXONxBwByjnQ== -"@swc/core@^1.3.23": +"@swc/core@^1.3.22": version "1.3.23" resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.23.tgz#62078fb161fad7edf7c8a8267cceccdeb2ab4a0d" integrity sha512-Aa7yw5+7ErOxr+G0J1eU2hkb9nEMSdt1Ye3isdAgg9mrsPuttk+cfLp6nP/Lux/VUnu5k4eOxeTy9UhjJhRAFw== @@ -1305,7 +1300,7 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== -"@types/chai@*", "@types/chai@^4.2.11": +"@types/chai@^4.2.11": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04" integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ== @@ -1348,18 +1343,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash.uniqby@^4.7.7": - version "4.7.7" - resolved "https://registry.yarnpkg.com/@types/lodash.uniqby/-/lodash.uniqby-4.7.7.tgz#48dbb652c41cc8fb30aa61a44174368081835ab5" - integrity sha512-sv2g6vkCIvEUsK5/Vq17haoZaisfj2EWW8mP7QWlnKi6dByoNmeuHDDXHR7sabuDqwO4gvU7ModIL22MmnOocg== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*": - version "4.14.182" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" - integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== - "@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1413,7 +1396,7 @@ "@types/glob" "*" "@types/node" "*" -"@types/sinon@*", "@types/sinon@10.0.11": +"@types/sinon@10.0.11": version "10.0.11" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.11.tgz#8245827b05d3fc57a6601bd35aee1f7ad330fc42" integrity sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g== @@ -1433,14 +1416,14 @@ "@types/expect" "^1.20.4" "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz#105788f299050c917eb85c4d9fd04b089e3740de" - integrity sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw== +"@typescript-eslint/eslint-plugin@^5.46.1": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.0.tgz#dadb79df3b0499699b155839fd6792f16897d910" + integrity sha512-AHZtlXAMGkDmyLuLZsRpH3p4G/1iARIwc/T0vIem2YB+xW6pZaXYXzCBnZSF/5fdM97R9QqZWZ+h3iW10XgevQ== dependencies: - "@typescript-eslint/scope-manager" "5.44.0" - "@typescript-eslint/type-utils" "5.44.0" - "@typescript-eslint/utils" "5.44.0" + "@typescript-eslint/scope-manager" "5.47.0" + "@typescript-eslint/type-utils" "5.47.0" + "@typescript-eslint/utils" "5.47.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1458,14 +1441,6 @@ "@typescript-eslint/typescript-estree" "5.46.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz#988c3f34b45b3474eb9ff0674c18309dedfc3e04" - integrity sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g== - dependencies: - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/visitor-keys" "5.44.0" - "@typescript-eslint/scope-manager@5.46.1": version "5.46.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" @@ -1474,38 +1449,33 @@ "@typescript-eslint/types" "5.46.1" "@typescript-eslint/visitor-keys" "5.46.1" -"@typescript-eslint/type-utils@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.44.0.tgz#bc5a6e8a0269850714a870c9268c038150dfb3c7" - integrity sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w== +"@typescript-eslint/scope-manager@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.47.0.tgz#f58144a6b0ff58b996f92172c488813aee9b09df" + integrity sha512-dvJab4bFf7JVvjPuh3sfBUWsiD73aiftKBpWSfi3sUkysDQ4W8x+ZcFpNp7Kgv0weldhpmMOZBjx1wKN8uWvAw== + dependencies: + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/visitor-keys" "5.47.0" + +"@typescript-eslint/type-utils@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.47.0.tgz#2b440979c574e317d3473225ae781f292c99e55d" + integrity sha512-1J+DFFrYoDUXQE1b7QjrNGARZE6uVhBqIvdaXTe5IN+NmEyD68qXR1qX1g2u4voA+nCaelQyG8w30SAOihhEYg== dependencies: - "@typescript-eslint/typescript-estree" "5.44.0" - "@typescript-eslint/utils" "5.44.0" + "@typescript-eslint/typescript-estree" "5.47.0" + "@typescript-eslint/utils" "5.47.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.44.0.tgz#f3f0b89aaff78f097a2927fe5688c07e786a0241" - integrity sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ== - "@typescript-eslint/types@5.46.1": version "5.46.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== -"@typescript-eslint/typescript-estree@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz#0461b386203e8d383bb1268b1ed1da9bc905b045" - integrity sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw== - dependencies: - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/visitor-keys" "5.44.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" +"@typescript-eslint/types@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.47.0.tgz#67490def406eaa023dbbd8da42ee0d0c9b5229d3" + integrity sha512-eslFG0Qy8wpGzDdYKu58CEr3WLkjwC5Usa6XbuV89ce/yN5RITLe1O8e+WFEuxnfftHiJImkkOBADj58ahRxSg== "@typescript-eslint/typescript-estree@5.46.1": version "5.46.1" @@ -1520,28 +1490,33 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.44.0.tgz#d733da4d79d6c30f1a68b531cdda1e0c1f00d52d" - integrity sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw== +"@typescript-eslint/typescript-estree@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.0.tgz#ed971a11c5c928646d6ba7fc9dfdd6e997649aca" + integrity sha512-LxfKCG4bsRGq60Sqqu+34QT5qT2TEAHvSCCJ321uBWywgE2dS0LKcu5u+3sMGo+Vy9UmLOhdTw5JHzePV/1y4Q== + dependencies: + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/visitor-keys" "5.47.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.47.0", "@typescript-eslint/utils@^5.46.1": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.47.0.tgz#b5005f7d2696769a1fdc1e00897005a25b3a0ec7" + integrity sha512-U9xcc0N7xINrCdGVPwABjbAKqx4GK67xuMV87toI+HUqgXj26m6RBp9UshEXcTrgCkdGYFzgKLt8kxu49RilDw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.44.0" - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/typescript-estree" "5.44.0" + "@typescript-eslint/scope-manager" "5.47.0" + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/typescript-estree" "5.47.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz#10740dc28902bb903d12ee3a005cc3a70207d433" - integrity sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ== - dependencies: - "@typescript-eslint/types" "5.44.0" - eslint-visitor-keys "^3.3.0" - "@typescript-eslint/visitor-keys@5.46.1": version "5.46.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" @@ -1550,6 +1525,14 @@ "@typescript-eslint/types" "5.46.1" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.0.tgz#4aca4efbdf6209c154df1f7599852d571b80bb45" + integrity sha512-ByPi5iMa6QqDXe/GmT/hR6MZtVPi0SqMQPDx15FczCBXJo/7M8T88xReOALAfpBLm+zxpPfmhuEvPb577JRAEg== + dependencies: + "@typescript-eslint/types" "5.47.0" + eslint-visitor-keys "^3.3.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -1629,10 +1612,10 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== +ajv@^8.0.1, ajv@^8.11.0, ajv@^8.11.2: + version "8.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" + integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2163,7 +2146,7 @@ chalk@^1.0.0: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2180,7 +2163,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3074,6 +3057,13 @@ eslint-plugin-prefer-arrow@^1.2.1: resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz#e7fbb3fa4cd84ff1015b9c51ad86550e55041041" integrity sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ== +eslint-plugin-sf-plugin@^1.2.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-sf-plugin/-/eslint-plugin-sf-plugin-1.2.3.tgz#c4b23b82c1bbf3550deed9325f224b12cc036656" + integrity sha512-VtNv6ZKbKAVfRD8QBts/wO52etu7hHpQclJyd+nVNhSGevvRKEKwAjVt4eC8kcPpAHDIbXlh2QnZbsaVvQhLRQ== + dependencies: + "@typescript-eslint/utils" "^5.46.1" + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -3334,20 +3324,6 @@ extract-stack@^2.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-2.0.0.tgz#11367bc865bfcd9bc0db3123e5edb57786f11f9b" integrity sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ== -fancy-test@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fancy-test/-/fancy-test-2.0.7.tgz#a5e8e23113384c5550914da4f429d10822622521" - integrity sha512-E9qiHMi/Wf3y0PLwoRbgr8SRTcvQY+6gx9d/qaVNT6N5AQ79iZr08ftY2Ki5KRC5LS02GoVD/CYT4t/KwwC/Pw== - dependencies: - "@types/chai" "*" - "@types/lodash" "*" - "@types/node" "*" - "@types/sinon" "*" - lodash "^4.17.13" - mock-stdin "^1.0.0" - nock "^13.0.0" - stdout-stderr "^0.1.9" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3546,7 +3522,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^10.0.0: +fs-extra@^10.0.0, fs-extra@^10.0.1: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== @@ -3868,7 +3844,7 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -handlebars@4.7.7: +handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -4152,10 +4128,10 @@ inquirer@^7.0.0: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.0.0: - version "8.2.4" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" - integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== +inquirer@^8.0.0, inquirer@^8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" + integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -4645,11 +4621,6 @@ json-stringify-nice@^1.1.4: resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -4883,7 +4854,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5257,11 +5228,6 @@ mocha@^9.1.3: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mock-stdin@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mock-stdin/-/mock-stdin-1.0.0.tgz#efcfaf4b18077e14541742fd758b9cae4e5365ea" - integrity sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q== - mri@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -5401,16 +5367,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.0.0: - version "13.2.7" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.7.tgz#c93933b61df42f4f4b3a07fde946a4e209c0c168" - integrity sha512-R6NUw7RIPtKwgK7jskuKoEi4VFMqIHtV2Uu9K/Uegc4TA5cqe+oNMYslZcUmnVNQCTG6wcSqUBaGTDd7sq5srg== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash "^4.17.21" - propagate "^2.0.0" - node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -6083,11 +6039,6 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -propagate@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" - integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== - psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -6735,14 +6686,6 @@ ssri@^9.0.0: dependencies: minipass "^3.1.1" -stdout-stderr@^0.1.9: - version "0.1.13" - resolved "https://registry.yarnpkg.com/stdout-stderr/-/stdout-stderr-0.1.13.tgz#54e3450f3d4c54086a49c0c7f8786a44d1844b6f" - integrity sha512-Xnt9/HHHYfjZ7NeQLvuQDyL1LnbsbddgMFKCuaQKwGCdJm8LnstZIXop+uOY36UR1UXXoHXfMbC1KlVdVd2JLA== - dependencies: - debug "^4.1.1" - strip-ansi "^6.0.0" - string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"