diff --git a/build.gradle.kts b/build.gradle.kts index 6456a4190..ffb5746f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ val junitVersion = "5.10.2" val mockkVersion = "1.13.10" val ibmMqVersion = "9.3.5.0" val jGraphTVersion = "1.5.2" -val zoweKotlinSdkVersion = "0.5.0-rc.8" +val zoweKotlinSdkVersion = "0.5.0-rc.11" val javaKeytarVersion = "1.0.0" repositories { diff --git a/gradle/sonar.gradle b/gradle/sonar.gradle index 20e92557d..dd1511abd 100644 --- a/gradle/sonar.gradle +++ b/gradle/sonar.gradle @@ -34,6 +34,7 @@ subprojects { properties { property "sonar.sources", "src/main/kotlin" property "sonar.tests", "src/test/kotlin" + property 'sonar.exclusions', "**/src/IC-231/**, **/src/IC-223/**" } } } diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/CredentialServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/config/connect/CredentialServiceImpl.kt index e152275c4..8fc145c13 100755 --- a/src/main/kotlin/org/zowe/explorer/config/connect/CredentialServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/CredentialServiceImpl.kt @@ -32,13 +32,30 @@ private fun createCredentialAttributes(key: String): CredentialAttributes { */ class CredentialServiceImpl : CredentialService { + /** + * Get PasswordSafe service + * Required for test purposes + */ + private fun getPasswordSafeService(): PasswordSafe { + return service() + } + /** * Get user credentials by connection config UUID. * @param connectionConfigUuid connection configuration universally unique identifier. * @return user credentials [Credentials] if they are. */ private fun getCredentials(connectionConfigUuid: String): Credentials? { - return service().get(createCredentialAttributes(connectionConfigUuid)) + val credentialAttributes = createCredentialAttributes(connectionConfigUuid) + var ret = service().get(credentialAttributes) + //Another attempt to read the password was added, since the saving of credentials occurs in a separate thread + //and depends on the operating system. If we saved the credentials and try to read them in another thread, + //but they have not yet actually been saved in storage. + if (ret==null){ + Thread.sleep(10) + ret = getPasswordSafeService().get(credentialAttributes) + } + return ret } /** @@ -67,7 +84,7 @@ class CredentialServiceImpl : CredentialService { val credentialAttributes = createCredentialAttributes(connectionConfigUuid) val credentials = Credentials(username, password) runBackgroundableTask("Setting user credentials") { - service().set(credentialAttributes, credentials) + getPasswordSafeService().set(credentialAttributes, credentials) sendTopic(CREDENTIALS_CHANGED).onChanged(connectionConfigUuid) } } @@ -78,7 +95,7 @@ class CredentialServiceImpl : CredentialService { */ override fun clearCredentials(connectionConfigUuid: String) { val credentialAttributes = createCredentialAttributes(connectionConfigUuid) - service().set(credentialAttributes, null) + getPasswordSafeService().set(credentialAttributes, null) } } diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt index 8ea3598c1..001c77bfe 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt @@ -287,7 +287,8 @@ class ConnectionDialog( ) .bindText(state::username) .validationOnApply { - it.text = it.text.trim() + if (it !is JPasswordField) + it.text = it.text.trim() validateForBlank(it) }.onApply { state.username = state.username.uppercase() @@ -299,7 +300,8 @@ class ConnectionDialog( .widthGroup(sameWidthLabelsGroup) passField = cell(JPasswordField()) .bindText(state::password) - .validationOnApply { validateForBlank(it) } + .validationOnApply { + validateForBlank(it) } .align(AlignX.FILL) } indent { diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt index a7e581157..03c1c5f4f 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt @@ -33,6 +33,7 @@ import org.zowe.explorer.utils.crudable.getAll import org.zowe.explorer.utils.isThe import org.zowe.explorer.utils.runWriteActionInEdtAndWait import org.zowe.explorer.utils.toMutableList +import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl import org.zowe.kotlinsdk.zowe.config.ZoweConfig import org.zowe.kotlinsdk.zowe.config.parseConfigJson import java.awt.event.MouseAdapter @@ -63,14 +64,23 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti showAndTestConnection()?.let { connectionsTableModel?.addRow(it) } } + /**Unable to save invalid URL + * Updates the selected profilef or current connection. + * If update is not possible(brocken URL), throws IllegalStateException exception + */ + @Throws(IllegalStateException::class) private fun ZoweConfig.updateFromState(state: ConnectionDialogState) { val uri = URI(state.connectionUrl) + if (uri.host.isNullOrEmpty()) + throw IllegalStateException("Unable to save invalid URL: ${state.connectionUrl}") + setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) host = uri.host - port = uri.port.toLong() + port = if (uri.port==-1) 10443 else uri.port.toLong() protocol = state.connectionUrl.split("://")[0] user = state.username password = state.password rejectUnauthorized = !state.isAllowSsl + restoreProfile() } /** @@ -81,7 +91,9 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti private fun updateZoweConfigIfNeeded(state: ConnectionDialogState?) { val res = showOkCancelDialog( title = "Zowe Config Update", - message = "Do you want to update zowe config file?\n${state?.zoweConfigPath}", + message = "Do you want to update zowe config file and credentials in secret store?" + + "\n${state?.connectionName}" + + "\n${state?.zoweConfigPath}", okText = "Yes", cancelText = "No" ) @@ -95,11 +107,21 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti val zoweConfig = parseConfigJson(configFile.inputStream) zoweConfig.extractSecureProperties(configFile.path.split("/").toTypedArray()) - zoweConfig.updateFromState(state) - runWriteActionInEdtAndWait { - zoweConfig.saveSecureProperties(configFile.path.split("/").toTypedArray()) - configFile.setBinaryContent(zoweConfig.toJson().toByteArray(configFile.charset)) + kotlin.runCatching { + zoweConfig.updateFromState(state) } + .onSuccess { + runWriteActionInEdtAndWait { + zoweConfig.setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) + zoweConfig.saveSecureProperties(configFile.path.split("/").toTypedArray()) + zoweConfig.restoreProfile() + configFile.setBinaryContent(zoweConfig.toJson().toByteArray(configFile.charset)) + } + } + .onFailure { + Messages.showErrorDialog("Unable to save invalid URL: ${state.connectionUrl}", "Invalid URL") + return + } } } @@ -112,8 +134,9 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti val state = showAndTestConnection(connectionsTableModel!![idx].apply { mode = DialogMode.UPDATE }) - state?.zoweConfigPath?.let { - zoweConfigStates[it] = state + state?.let { + if (it.zoweConfigPath != null) + zoweConfigStates[it.connectionName] = state } if (state != null) { connectionsTableModel?.set(idx, state) @@ -319,7 +342,7 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti /** Check are the Credentials and Connections sandboxes modified */ override fun isModified(): Boolean { return isSandboxModified() - || isSandboxModified() + || isSandboxModified() } override fun cancel() { diff --git a/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt b/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt index e948e6216..66750a674 100644 --- a/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt +++ b/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt @@ -21,12 +21,12 @@ import org.zowe.explorer.explorer.ui.UssDirNode import org.zowe.explorer.explorer.ui.UssFileNode import org.zowe.explorer.utils.crudable.Crudable import org.zowe.explorer.utils.crudable.find -import org.zowe.kotlinsdk.DatasetOrganization import javax.swing.JComponent import javax.swing.JPasswordField import javax.swing.JTextField -private val urlRegex = Regex("^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]") +private val urlRegex = + Regex("^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!,.;]*(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?") private val maskRegex = Regex("^[A-Za-z\\$\\*%@#][A-Za-z0-9\\-\\$\\*%@#]{0,7}") private val ussPathRegex = Regex("^/$|^(/[^/]+)+$") private val forbiddenSymbol = "/" @@ -81,7 +81,10 @@ fun validateForPassword(password: String, component: JPasswordField): Validation * @param component the component to check the text in */ fun validateForBlank(component: JTextField): ValidationInfo? { - return validateForBlank(component.text, component) + return if(component is JPasswordField){ + validateForBlank(String(component.password).trim(), component) + } + else validateForBlank(component.text, component) } /** diff --git a/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt b/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt index aa8af7b57..8896d2f27 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt @@ -23,8 +23,10 @@ import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.explorer.EXPLORER_NOTIFICATION_GROUP_ID import org.zowe.explorer.utils.subscribe import org.zowe.explorer.zowe.service.* +import org.zowe.explorer.zowe.service.ZoweConfigService.Companion.lock import org.zowe.kotlinsdk.zowe.config.ZoweConfig import java.util.regex.Pattern +import kotlin.concurrent.write const val ZOWE_CONFIG_NAME = "zowe.config.json" @@ -71,7 +73,7 @@ fun showNotificationForAddUpdateZoweConfigIfNeeded(project: Project, type: ZoweC fun showDialogForDeleteZoweConfigIfNeeded(project: Project, type: ZoweConfigType) { val zoweConfigService = project.service() val zoweConfigState = zoweConfigService.getZoweConfigState(type = type) - if (zoweConfigState != ZoweConfigState.NEED_TO_ADD || zoweConfigState != ZoweConfigState.NOT_EXISTS) { + if (zoweConfigState != ZoweConfigState.NEED_TO_ADD && zoweConfigState != ZoweConfigState.NOT_EXISTS) { val choice = Messages.showDialog( project, "$type Zowe config file has been deleted.\n" + @@ -79,7 +81,7 @@ fun showDialogForDeleteZoweConfigIfNeeded(project: Project, type: ZoweConfigType "If you decide to leave the connection, it will be converted to a regular connection (username will be visible).", "Deleting Zowe Config connection", arrayOf( - "Delete Connection", "Keep Connection" + "Delete Connection(s)", "Keep Connection(s)" ), 0, AllIcons.General.QuestionDialog @@ -88,10 +90,12 @@ fun showDialogForDeleteZoweConfigIfNeeded(project: Project, type: ZoweConfigType zoweConfigService.deleteZoweConfig(type) } } - if (type == ZoweConfigType.LOCAL) - zoweConfigService.localZoweConfig = null - else - zoweConfigService.globalZoweConfig = null + lock.write { + if (type == ZoweConfigType.LOCAL) + zoweConfigService.localZoweConfig = null + else + zoweConfigService.globalZoweConfig = null + } zoweConfigService.checkAndRemoveOldZoweConnection(type) } diff --git a/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt b/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt index b18362c06..f776dcb8c 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt @@ -16,7 +16,9 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.DumbAwareAction +import org.zowe.explorer.utils.write import org.zowe.explorer.zowe.service.ZoweConfigService +import org.zowe.explorer.zowe.service.ZoweConfigService.Companion.lock import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl import org.zowe.explorer.zowe.service.ZoweConfigState import org.zowe.explorer.zowe.service.ZoweConfigType @@ -29,8 +31,7 @@ import org.zowe.kotlinsdk.zowe.config.parseConfigJson * @since 2021-02-12 */ class UpdateZoweConfigAction : DumbAwareAction() { - - override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: let { @@ -75,12 +76,11 @@ class UpdateZoweConfigAction : DumbAwareAction() { type = ZoweConfigType.LOCAL val zoweConfigService = project.service() - - val prevZoweConfig = if (type == ZoweConfigType.LOCAL) - zoweConfigService.localZoweConfig - else - zoweConfigService.globalZoweConfig - runCatching { + lock.write { + val prevZoweConfig = if (type == ZoweConfigType.LOCAL) + zoweConfigService.localZoweConfig + else + zoweConfigService.globalZoweConfig if (type == ZoweConfigType.LOCAL) { zoweConfigService.localZoweConfig = parseConfigJson(editor.document.text) zoweConfigService.localZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) @@ -88,14 +88,14 @@ class UpdateZoweConfigAction : DumbAwareAction() { zoweConfigService.globalZoweConfig = parseConfigJson(editor.document.text) zoweConfigService.globalZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) } - } - val zoweState = zoweConfigService.getZoweConfigState(false, type = type) - e.presentation.isEnabledAndVisible = - zoweState == ZoweConfigState.NEED_TO_UPDATE || zoweState == ZoweConfigState.NEED_TO_ADD - if (type == ZoweConfigType.LOCAL) { - zoweConfigService.localZoweConfig = prevZoweConfig - } else { - zoweConfigService.globalZoweConfig = prevZoweConfig + val zoweState = zoweConfigService.getZoweConfigState(false, type = type) + e.presentation.isEnabledAndVisible = + zoweState == ZoweConfigState.NEED_TO_UPDATE || zoweState == ZoweConfigState.NEED_TO_ADD + if (type == ZoweConfigType.LOCAL) { + zoweConfigService.localZoweConfig = prevZoweConfig + } else { + zoweConfigService.globalZoweConfig = prevZoweConfig + } } } } diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt index 090d93c27..5065a1051 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt @@ -15,6 +15,7 @@ import com.intellij.util.messages.Topic import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState import org.zowe.kotlinsdk.zowe.config.ZoweConfig +import java.util.concurrent.locks.ReentrantReadWriteLock /** @@ -78,7 +79,7 @@ interface ZoweConfigService { * @param checkConnection - Verify zowe connection by sending info request if true. * @return - ConnectionConfig that was added or updated. */ - fun addOrUpdateZoweConfig(scanProject: Boolean = true, checkConnection: Boolean = true, type: ZoweConfigType): ConnectionConfig? + fun addOrUpdateZoweConfig(scanProject: Boolean = true, checkConnection: Boolean = true, type: ZoweConfigType) /** * Deletes connection config related to zoweConnection @@ -102,6 +103,7 @@ interface ZoweConfigService { companion object { fun getInstance(project: Project): ZoweConfigService = project.getService(ZoweConfigService::class.java) + val lock = ReentrantReadWriteLock() } } diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt index 121f5dafe..6ab92b1b6 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -23,26 +23,25 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFileManager import org.zowe.explorer.config.ConfigService -import org.zowe.explorer.config.connect.ConnectionConfig -import org.zowe.explorer.config.connect.CredentialService -import org.zowe.explorer.config.connect.getPassword -import org.zowe.explorer.config.connect.getUsername +import org.zowe.explorer.config.connect.* import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState import org.zowe.explorer.config.connect.ui.zosmf.ZOSMFConnectionConfigurable.Companion.warningMessageForDeleteConfig -import org.zowe.explorer.config.connect.whoAmI import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.config.ws.JesWorkingSetConfig import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.operations.InfoOperation import org.zowe.explorer.dataops.operations.ZOSInfoOperation import org.zowe.explorer.explorer.EXPLORER_NOTIFICATION_GROUP_ID +import org.zowe.explorer.utils.* import org.zowe.explorer.utils.crudable.find import org.zowe.explorer.utils.crudable.getAll import org.zowe.explorer.utils.runTask import org.zowe.explorer.utils.sendTopic import org.zowe.explorer.utils.toMutableList import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME +import org.zowe.explorer.zowe.service.ZoweConfigService.Companion.lock import org.zowe.kotlinsdk.annotations.ZVersion +import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.kotlinsdk.zowe.config.ZoweConfig import org.zowe.kotlinsdk.zowe.config.parseConfigJson import java.io.File @@ -84,11 +83,11 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService /** * Returns Zowe connection name */ - fun getZoweConnectionName(myProject: Project?, type: ZoweConfigType): String { - return if (type == ZoweConfigType.LOCAL) - "${ZOWE_PROJECT_PREFIX}${type}-zosmf/${myProject?.name}" + fun getZoweConnectionName(myProject: Project?, type: ZoweConfigType, profileName: String = "zosmf"): String { + return if (type == ZoweConfigType.LOCAL) + "${ZOWE_PROJECT_PREFIX}${type}-${profileName}/${myProject?.name}" else - "${ZOWE_PROJECT_PREFIX}${type}-zosmf" + "${ZOWE_PROJECT_PREFIX}${type}-${profileName}" } /** @@ -100,6 +99,16 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService else System.getProperty("user.home").replace("((\\*)|(/*))$", "") + "/.zowe/" + ZOWE_CONFIG_NAME } + + /** + * Returns profile name for current connection + */ + fun getProfileNameFromConnName(connName: String): String { + var profileName = connName.replace(Regex("^$ZOWE_PROJECT_PREFIX((local)|(global))+(-)"), "") + profileName = profileName.replace(Regex("/.*$"), "") + return profileName + } + } private val configCrudable = ConfigService.instance.crudable @@ -138,10 +147,12 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService zoweFile.inputStream.use { zoweFileInputStream -> parseConfigJson(zoweFileInputStream).also { tmpZoweConfig -> tmpZoweConfig.extractSecureProperties(zoweFile.path.split("/").toTypedArray()) - if (type == ZoweConfigType.LOCAL) - localZoweConfig = tmpZoweConfig - else - globalZoweConfig = tmpZoweConfig + lock.write { + if (type == ZoweConfigType.LOCAL) + localZoweConfig = tmpZoweConfig + else + globalZoweConfig = tmpZoweConfig + } } } } catch (e: Exception) { @@ -149,13 +160,33 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } + /** + * Finds A/ll existing connections related to zowe.config.json. + * @return list of found ConnectionConfigs related to zowe config if it exists or null otherwise. + */ + private fun findAllZosmfExistingConnection(type: ZoweConfigType): List? { + val zoweConnectionList = configCrudable.find { + val pattern = + if (type == ZoweConfigType.LOCAL) { + Regex("^(" + ZOWE_PROJECT_PREFIX + type + "-).*(/" + myProject.name + ")$") + } else { + Regex("^(" + ZOWE_PROJECT_PREFIX + type+ "-).*") + } + it.name.matches(pattern) && it.zoweConfigPath == getZoweConfigLocation(myProject, type) + }.collect(Collectors.toList()) + return if (zoweConnectionList.isEmpty()) null else zoweConnectionList + } + /** * Finds existing connection config related to zowe.config.json. * @return ConnectionConfig instance related to zowe config if it exists or null otherwise. */ - private fun findExistingConnection(type: ZoweConfigType): ConnectionConfig? { + private fun findExistingConnection(type: ZoweConfigType, profileName: String): ConnectionConfig? { val zoweConnectionList = configCrudable.find { - it.name == getZoweConnectionName(myProject, type) + it.name == getZoweConnectionName(myProject, type, profileName) && it.zoweConfigPath == getZoweConfigLocation( + myProject, + type + ) }.collect(Collectors.toList()) return if (zoweConnectionList.isEmpty()) null else zoweConnectionList[0] } @@ -165,8 +196,8 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService * then extracts existing uuid and generates a new one otherwise. * @return created or existing uuid. */ - private fun getOrCreateUuid(type: ZoweConfigType): String { - return findExistingConnection(type)?.uuid ?: UUID.randomUUID().toString() + private fun getOrCreateUuid(type: ZoweConfigType, profileName: String): String { + return findExistingConnection(type, profileName)?.uuid ?: UUID.randomUUID().toString() } /** @@ -181,7 +212,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService .apply { addAction(object : DumbAwareAction("Add Anyway") { override fun actionPerformed(e: AnActionEvent) { - addOrUpdateZoweConfig(checkConnection = false, type = type) + addOrUpdateZoweConfig(checkConnection = false,type = type) hideBalloon() } }) @@ -230,11 +261,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService /** * @see ZoweConfigService.addOrUpdateZoweConfig */ - override fun addOrUpdateZoweConfig( - scanProject: Boolean, - checkConnection: Boolean, - type: ZoweConfigType - ): ConnectionConfig? { + override fun addOrUpdateZoweConfig(scanProject: Boolean, checkConnection: Boolean, type: ZoweConfigType) { return try { val zoweConfig = if (scanProject) { scanForZoweConfig(type) @@ -243,65 +270,105 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService else this.globalZoweConfig zoweConfig ?: throw Exception("Cannot get $type Zowe config") - val username = zoweConfig.user ?: throw Exception("Cannot get username for $type Zowe config") - val password = zoweConfig.password ?: throw Exception("Cannot get password for $type Zowe config") - val zoweConnection = findExistingConnection(type)?.let { - zoweConfig.toConnectionConfig(it.uuid, it.zVersion, type = type) - } ?: zoweConfig.toConnectionConfig(UUID.randomUUID().toString(), type = type) - CredentialService.instance.setCredentials(zoweConnection.uuid, username, password) if (checkConnection) { - try { - testAndPrepareConnection(zoweConnection) - } catch (t: Throwable) { - notifyUiOnConnectionFailure("Connection to ${zoweConnection.url} failed.", t.message ?: "", type) - return null + val failedURLs = testAndPrepareAllZosmfConnections(zoweConfig, type) + if (failedURLs.isNotEmpty()) { + val andMore = if (failedURLs.size > 3) + "..." + else + "" + notifyUiOnConnectionFailure( + "Connection failed to:", + "${failedURLs.joinToString(separator = ",

")} ${andMore}", + type + ) + return } } - - val connectionOpt = configCrudable.addOrUpdate(zoweConnection) - return if (connectionOpt.isEmpty) null else connectionOpt.get().also { - CredentialService.instance.setCredentials(it.uuid, username, password) - var topic = if (type == ZoweConfigType.LOCAL) - LOCAL_ZOWE_CONFIG_CHANGED - else - GLOBAL_ZOWE_CONFIG_CHANGED - sendTopic(topic).onConfigSaved(zoweConfig, zoweConnection) + for (zosmfConnection in zoweConfig.getListOfZosmfConections()) { + val zoweConnection = prepareConnection(zosmfConnection, type) + val connectionOpt = configCrudable.addOrUpdate(zoweConnection) + if (!connectionOpt.isEmpty) { + var topic = if (type == ZoweConfigType.LOCAL) + LOCAL_ZOWE_CONFIG_CHANGED + else + GLOBAL_ZOWE_CONFIG_CHANGED + sendTopic(topic).onConfigSaved(zoweConfig, zoweConnection) + } } + } catch (e: Exception) { notifyError(e) - null } } + /** + * Convert ZOSConnection to ConnectionConfig + * @param zosmfConnection connection to prepare. + * @param type of zowe config + * @return prepared ConnectionConfig + */ + private fun prepareConnection(zosmfConnection: ZOSConnection, type: ZoweConfigType): ConnectionConfig { + val username = zosmfConnection.user + val password = zosmfConnection.password + val zoweConnection = findExistingConnection(type, zosmfConnection.profileName)?.let { + zosmfConnection.toConnectionConfig(it.uuid, it.zVersion, type = type) + } ?: zosmfConnection.toConnectionConfig(UUID.randomUUID().toString(), type = type) + CredentialService.instance.setCredentials(zoweConnection.uuid, username, password) + return zoweConnection + } + + /** + * Convert all zosmf connections from zowe config file to ConnectionConfig and tests them + * @param zoweConfig + * @param type of zowe config + * @return list of URLs which did not pass the test + */ + private fun testAndPrepareAllZosmfConnections(zoweConfig: ZoweConfig, type: ZoweConfigType): List { + var failedURLs = mutableListOf() + for (zosmfConnection in zoweConfig.getListOfZosmfConections()) { + val zoweConnection = prepareConnection(zosmfConnection, type) + try { + testAndPrepareConnection(zoweConnection) + } catch (t: Throwable) { + failedURLs.add(zoweConnection.url) + } + } + return failedURLs + } + /** * @see ZoweConfigService.deleteZoweConfig */ override fun deleteZoweConfig(type: ZoweConfigType) { try { - val zoweConnection = findExistingConnection(type) ?: throw Exception("Cannot get Zowe config") - val filesWorkingSets = configCrudable.getAll().toMutableList() - val filesWsUsages = filesWorkingSets.filter { filesWsConfig -> - filesWsConfig.connectionConfigUuid == zoweConnection.uuid - } + val zoweConnections = findAllZosmfExistingConnection(type) ?: throw Exception("Cannot find any zosmf connections") - val jesWorkingSet = configCrudable.getAll().toMutableList() - val jesWsUsages = jesWorkingSet.filter { jesWsConfig -> - jesWsConfig.connectionConfigUuid == zoweConnection.uuid - } + zoweConnections.forEach { zoweConnection -> - if (filesWsUsages.isEmpty() && jesWsUsages.isEmpty()) { - CredentialService.instance.clearCredentials(zoweConnection.uuid) - configCrudable.delete(zoweConnection) - return - } + val filesWorkingSets = configCrudable.getAll().toMutableList() + val filesWsUsages = filesWorkingSets.filter { filesWsConfig -> + filesWsConfig.connectionConfigUuid == zoweConnection.uuid + } + + val jesWorkingSet = configCrudable.getAll().toMutableList() + val jesWsUsages = jesWorkingSet.filter { jesWsConfig -> + jesWsConfig.connectionConfigUuid == zoweConnection.uuid + } - val ret = warningMessageForDeleteConfig(filesWsUsages, jesWsUsages) + if (filesWsUsages.isEmpty() && jesWsUsages.isEmpty()) { + CredentialService.instance.clearCredentials(zoweConnection.uuid) + configCrudable.delete(zoweConnection) + } else { + val ret = warningMessageForDeleteConfig(filesWsUsages, jesWsUsages) - if (ret == Messages.OK) { - CredentialService.instance.clearCredentials(zoweConnection.uuid) - configCrudable.delete(zoweConnection) + if (ret == Messages.OK) { + CredentialService.instance.clearCredentials(zoweConnection.uuid) + configCrudable.delete(zoweConnection) + } + } } } catch (e: Exception) { @@ -321,15 +388,17 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService createZoweSchemaJsonIfNotExists() val urlRegex = - "(https?:\\/\\/)(www\\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})\b?([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)" + "^(https?|http)://([-a-zA-Z0-9+&@#/%?=~_|!,.;]*)(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?" val pattern: Pattern = Pattern.compile(urlRegex) val matcher: Matcher = pattern.matcher(state.connectionUrl) var host = "localhost" - var port = "443" + var port = "10443" if (matcher.matches()) { - host = matcher.group(3) - port = matcher.group(4).substring(1) + if (matcher.group(2) != null) + host = matcher.group(2) + if (matcher.group(3) != null) + port = matcher.group(3).substring(1) } val content = getResourceStream("files/${ZOWE_CONFIG_NAME}") @@ -353,7 +422,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService val allConnections = configCrudable.getAll().toList() val allConnectionsNames: MutableList = allConnections.map { it.name }.toMutableList() - allConnections.filter { it.zoweConfigPath == getZoweConfigLocation(myProject, type) }.forEach { + allConnections.filter {it.zoweConfigPath == getZoweConfigLocation(myProject, type)}.forEach { var index = 1 var newName = it.name while (allConnectionsNames.contains(newName)) { @@ -382,20 +451,20 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService * @param uuid - uuid returned connection. * @return converted ConnectionConfig. */ - fun ZoweConfig.toConnectionConfig( + fun ZOSConnection.toConnectionConfig( uuid: String, zVersion: ZVersion = ZVersion.ZOS_2_1, owner: String = "", type: ZoweConfigType ): ConnectionConfig { val basePath = if (basePath.last() == '/') basePath.dropLast(1) else basePath - val domain = if (port == null) host else "${host}:${port}" + val domain = "${host}:${zosmfPort}" val zoweUrl = "${protocol}://${domain}${basePath}" val isAllowSelfSigned = !(rejectUnauthorized ?: false) return ConnectionConfig( uuid, - getZoweConnectionName(myProject, type), + getZoweConnectionName(myProject, type, profileName), zoweUrl, isAllowSelfSigned, zVersion, @@ -404,15 +473,6 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService ) } - /** - * Converts to connection config with existing connection config uuid. - * related to zowe config or generates a new one. - * @return converted ConnectionConfig. - */ - fun ZoweConfig.toConnectionConfig(zVersion: ZVersion = ZVersion.ZOS_2_1, type: ZoweConfigType): ConnectionConfig = - toConnectionConfig(getOrCreateUuid(type), zVersion, type = type) - - /** * @see ZoweConfigService.getZoweConfigState */ @@ -429,22 +489,51 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService else globalZoweConfig ?: return ZoweConfigState.NOT_EXISTS - val existingConnection = findExistingConnection(type) ?: return ZoweConfigState.NEED_TO_ADD - val newConnection = zoweConfig.toConnectionConfig( - existingConnection.uuid, existingConnection.zVersion, existingConnection.owner, type = type - ) - - val zoweUsername = zoweConfig.user ?: return ZoweConfigState.ERROR - val zowePassword = zoweConfig.password ?: return ZoweConfigState.ERROR - - return if (existingConnection == newConnection && - getUsername(newConnection) == zoweUsername && - getPassword(newConnection) == zowePassword - ) { - ZoweConfigState.SYNCHRONIZED - } else { - ZoweConfigState.NEED_TO_UPDATE + findAllZosmfExistingConnection(type) ?: return ZoweConfigState.NEED_TO_ADD + var ret = ZoweConfigState.SYNCHRONIZED + + for (zosConnection in zoweConfig.getListOfZosmfConections()) { + val existingConnection = + findExistingConnection(type, zosConnection.profileName) + if (existingConnection == null) + ret = setZoweConfigState(ret, ZoweConfigState.NEED_TO_ADD) + else { + val newConnectionList = zoweConfig.getListOfZosmfConections() + .filter { it.profileName == getProfileNameFromConnName(existingConnection.name) } + val newConnection = if (newConnectionList.isNotEmpty()) { + newConnectionList[0].toConnectionConfig( + existingConnection.uuid.toString(), existingConnection.zVersion, existingConnection.owner, type = type + ) + } else { + ret = setZoweConfigState(ret, ZoweConfigState.NEED_TO_ADD) + continue + } + val zoweUsername = zosConnection.user + val zowePassword = zosConnection.password + + ret = if (existingConnection == newConnection && + getUsername(newConnection) == zoweUsername && + getPassword(newConnection) == zowePassword + ) { + setZoweConfigState(ret, ZoweConfigState.SYNCHRONIZED) + } else { + setZoweConfigState(ret, ZoweConfigState.NEED_TO_UPDATE) + } + } } + return ret + } + + /** + * Returns the resulting ZoweConfigState depending on the previous and new ones + * @param prev previous state + * @param curr new state + * @return resulting ZoweConfigState + */ + private fun setZoweConfigState(prev: ZoweConfigState, curr: ZoweConfigState): ZoweConfigState { + return if (prev == ZoweConfigState.ERROR || curr == ZoweConfigState.SYNCHRONIZED) + prev + else curr } } diff --git a/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt index 30f73672d..717348675 100644 --- a/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt @@ -10,6 +10,7 @@ package org.zowe.explorer.config +import com.google.gson.Gson import com.intellij.notification.Notification import com.intellij.notification.Notifications import com.intellij.openapi.application.ApplicationManager @@ -25,6 +26,7 @@ import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import io.mockk.* import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.config.connect.Credentials import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState import org.zowe.explorer.config.connect.whoAmI import org.zowe.explorer.config.ws.FilesWorkingSetConfig @@ -37,9 +39,10 @@ import org.zowe.explorer.explorer.Explorer import org.zowe.explorer.explorer.WorkingSet import org.zowe.explorer.testutils.WithApplicationShouldSpec import org.zowe.explorer.testutils.testServiceImpl.TestDataOpsManagerImpl -import org.zowe.explorer.utils.crudable.Crudable -import org.zowe.explorer.utils.crudable.getAll +import org.zowe.explorer.utils.crudable.* +import org.zowe.explorer.utils.runIfTrue import org.zowe.explorer.utils.service +import org.zowe.explorer.utils.validateForBlank import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl.Companion.getZoweConfigLocation @@ -49,8 +52,10 @@ import org.zowe.explorer.zowe.service.ZoweConfigType import org.zowe.kotlinsdk.InfoResponse import org.zowe.kotlinsdk.SystemsResponse import org.zowe.kotlinsdk.annotations.ZVersion +import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.kotlinsdk.zowe.config.KeytarWrapper import org.zowe.kotlinsdk.zowe.config.ZoweConfig +import org.zowe.kotlinsdk.zowe.config.encodeToBase64 import org.zowe.kotlinsdk.zowe.config.parseConfigJson import java.io.InputStream import java.nio.file.Files @@ -58,9 +63,13 @@ import java.nio.file.Path import java.util.* import java.util.stream.Stream import javax.swing.Icon +import javax.swing.JPasswordField +import javax.swing.JTextField import kotlin.reflect.KFunction import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaField class ZoweConfigTestSpec : WithApplicationShouldSpec({ val tmpZoweConfFile = "test/$ZOWE_CONFIG_NAME" @@ -96,12 +105,7 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ val connectionId = "000000000000" val connection = ConnectionConfig( - "ID$connectionId", - connectionId, - "URL$connectionId", - true, - ZVersion.ZOS_2_4, - zoweConfigPath = "/zowe/config/path" + "ID$connectionId", connectionId, "URL$connectionId", true, ZVersion.ZOS_2_4, zoweConfigPath = "/zowe/config/path" ) val crudableMockk = mockk() every { crudableMockk.getAll() } returns Stream.of() @@ -217,6 +221,17 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ every { zoweConfigMock.host } returns "111.111.111.111" every { zoweConfigMock.protocol } returns "https" every { zoweConfigMock.rejectUnauthorized } returns null + val zossConn1 = ZOSConnection( + "111.111.111.111", + "10443", + "testUser", + "testPassword", + rejectUnauthorized = true, + basePath = "/base/config/path/", + profileName = "zosmf" + ) + val zosConnList1 = mutableListOf(zossConn1) + every { zoweConfigMock.getListOfZosmfConections() } returns zosConnList1 val parseConfigJsonFun: (InputStream) -> ZoweConfig = ::parseConfigJson mockkStatic(parseConfigJsonFun as KFunction<*>) @@ -235,19 +250,16 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ if (infoRes.zosVersion == "throw1") { throw Throwable("Test performInfoOperation throw") } - @Suppress("UNCHECKED_CAST") - return SystemsResponse(numRows = 1) as R + @Suppress("UNCHECKED_CAST") return SystemsResponse(numRows = 1) as R } if (operation is ZOSInfoOperation) { isZOSInfoCalled = true if (infoRes.zosVersion == "throw2") { throw Throwable("Test performOperation throw") } - @Suppress("UNCHECKED_CAST") - return infoRes as R + @Suppress("UNCHECKED_CAST") return infoRes as R } - @Suppress("UNCHECKED_CAST") - return InfoResponse() as R + @Suppress("UNCHECKED_CAST") return InfoResponse() as R } } @@ -258,13 +270,13 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ getZoweConnectionName(mockedProject, ZoweConfigType.LOCAL) shouldBe "zowe-local-zosmf/testProj" getZoweConnectionName(mockedProject, ZoweConfigType.GLOBAL) shouldBe "zowe-global-zosmf" getZoweConnectionName(null, ZoweConfigType.GLOBAL) shouldBe "zowe-global-zosmf" + getZoweConnectionName(null, ZoweConfigType.LOCAL) shouldBe "zowe-local-zosmf/null" } should("getZoweConfigLocation") { getZoweConfigLocation(mockedProject, ZoweConfigType.LOCAL) shouldBe "test/zowe.config.json" getZoweConfigLocation( - mockedProject, - ZoweConfigType.GLOBAL + mockedProject, ZoweConfigType.GLOBAL ) shouldBe System.getProperty("user.home").replace("((\\*)|(/*))$", "") + "/.zowe/" + ZOWE_CONFIG_NAME getZoweConfigLocation(null, ZoweConfigType.LOCAL) shouldBe "null/zowe.config.json" } @@ -426,9 +438,7 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ should("addOrUpdateZoweConfig throw Cannot get Zowe config") { mockedZoweConfigService.addOrUpdateZoweConfig( - scanProject = false, - checkConnection = true, - type = ZoweConfigType.GLOBAL + scanProject = false, checkConnection = true, type = ZoweConfigType.GLOBAL ) notified shouldBe true } @@ -436,9 +446,7 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ should("addOrUpdateZoweConfig throw Cannot get password") { every { zoweConfigMock.password } returns null mockedZoweConfigService.addOrUpdateZoweConfig( - scanProject = false, - checkConnection = true, - type = ZoweConfigType.LOCAL + scanProject = false, checkConnection = true, type = ZoweConfigType.LOCAL ) every { zoweConfigMock.password } returns "password" notified shouldBe true @@ -447,9 +455,7 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ should("addOrUpdateZoweConfig throw Cannot get username") { every { zoweConfigMock.user } returns null mockedZoweConfigService.addOrUpdateZoweConfig( - scanProject = false, - checkConnection = true, - type = ZoweConfigType.LOCAL + scanProject = false, checkConnection = true, type = ZoweConfigType.LOCAL ) every { zoweConfigMock.user } returns "ZoweUserName" notified shouldBe true @@ -458,11 +464,11 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ should("getOrCreateUuid") { mockedZoweConfigService::class.declaredMemberFunctions.find { it.name == "getOrCreateUuid" }?.let { it.isAccessible = true - it.call(mockedZoweConfigService, ZoweConfigType.LOCAL) shouldBe "ID000000000000" + it.call(mockedZoweConfigService, ZoweConfigType.LOCAL, "zosmf") shouldBe "ID000000000000" every { crudableMockk.find(any(), any()) } answers { emptyList().stream() } - val randomUUID = it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL) + val randomUUID = it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL, "zosmf") randomUUID shouldNotBe null randomUUID shouldNotBe "ID000000000000" } @@ -470,13 +476,22 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ should("addOrUpdateZoweConfig New ConnectionConfig throw on check") { mockedZoweConfigService.addOrUpdateZoweConfig( - scanProject = false, - checkConnection = true, - type = ZoweConfigType.LOCAL + scanProject = false, checkConnection = true, type = ZoweConfigType.LOCAL ) notified shouldBe true } + should("scanForZoweConfig global throw") { + mockedZoweConfigService::class.declaredMemberFunctions.find { it.name == "scanForZoweConfig" }?.let { + it.isAccessible = true + try { + it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL) + } catch (e: Exception) { + e.cause.toString() shouldContain "Cannot parse ${ZoweConfigType.GLOBAL} Zowe config file" + } + } + } + should("scanForZoweConfig throw") { clearMocks(zoweConfigMock) every { zoweConfigMock.extractSecureProperties(any>(), any()) } answers { @@ -498,8 +513,9 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ } should("getZoweConfigState") { + val zosConnList = mutableListOf() for (type in ZoweConfigType.entries) { - connection.name = "testConnection" + connection.name = getZoweConnectionName(mockedProject, type)//"zowe-$type-zosmf/untitled" mockedZoweConfigService.globalZoweConfig = null mockedZoweConfigService.localZoweConfig = null mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.NOT_EXISTS @@ -513,32 +529,63 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ every { zoweConfigMock.user } returns "testUser" every { zoweConfigMock.password } returns "testPassword" every { zoweConfigMock.basePath } returns "/base/config/path/" - every { zoweConfigMock.port } returns null + every { zoweConfigMock.port } returns 10443 every { zoweConfigMock.host } returns "111.111.111.111" every { zoweConfigMock.protocol } returns "https" every { zoweConfigMock.rejectUnauthorized } returns true + val conn = ZOSConnection( + "111.111.111.111", + "10443", + "testUser", + "testPassword", + rejectUnauthorized = true, + basePath = "/base/config/path/", + profileName = "zosmf" + ) + zosConnList.clear() + zosConnList.add(conn) + every { zoweConfigMock.getListOfZosmfConections() } returns zosConnList clearMocks(crudableMockk) every { crudableMockk.find(any(), any()) } answers { listOf(connection).stream() } mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.NEED_TO_UPDATE connection.name = getZoweConnectionName(mockedProject, type) - connection.url = "https://111.111.111.111/base/config/path" + connection.url = "https://111.111.111.111:10443/base/config/path" connection.isAllowSelfSigned = false connection.zVersion = ZVersion.ZOS_2_1 connection.zoweConfigPath = getZoweConfigLocation(mockedProject, type) connection.owner = "" mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.SYNCHRONIZED every { zoweConfigMock.password } returns "wrongPass" + zosConnList.clear() + zosConnList.add( + ZOSConnection( + "111.111.111.111", + "10443", + "testUser", + "wrongPass", + rejectUnauthorized = true, + basePath = "/base/config/path/", + profileName = "zosmf" + ) + ) mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.NEED_TO_UPDATE every { zoweConfigMock.password } returns "testPassword" every { zoweConfigMock.user } returns "wrongUser" + zosConnList.clear() + zosConnList.add( + ZOSConnection( + "111.111.111.111", + "10443", + "wrongUser", + "testPassword", + rejectUnauthorized = true, + basePath = "/base/config/path/", + profileName = "zosmf" + ) + ) mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.NEED_TO_UPDATE - every { zoweConfigMock.password } returns null - mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.ERROR - every { zoweConfigMock.password } returns "testPassword" - every { zoweConfigMock.user } returns null - mockedZoweConfigService.getZoweConfigState(false, type) shouldBe ZoweConfigState.ERROR every { zoweConfigMock.user } returns "testUser" } } @@ -560,6 +607,119 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ mockedZoweConfigService.deleteZoweConfig(type = ZoweConfigType.LOCAL) notified shouldBe true } + + fun createSinglePassword(filePath: String, user: String, password: String): String { + val credentialsMap = mutableMapOf>( + Pair( + filePath, mapOf( + Pair("profiles.base.properties.user", user), + Pair("profiles.base.properties.password", password) + ) + ) + ) + return Gson().toJson(credentialsMap).encodeToBase64() + } + + fun makeCrudableWithoutListeners( + withCredentials: Boolean, + credentialsGetter: () -> MutableList = { mutableListOf() }, + stateGetter: () -> ConfigStateV2 + ): Crudable { + val crudableLists = CrudableLists(addFilter = object : AddFilter { + override operator fun invoke(clazz: Class, addingRow: T): Boolean { + return ConfigService.instance.getConfigDeclaration(clazz).getDecider().canAdd(addingRow) + } + }, updateFilter = object : UpdateFilter { + override operator fun invoke(clazz: Class, currentRow: T, updatingRow: T): Boolean { + return ConfigService.instance.getConfigDeclaration(clazz).getDecider().canUpdate(currentRow, updatingRow) + } + }, nextUuidProvider = { UUID.randomUUID().toString() }, getListByClass = { + if (it == Credentials::class.java) { + withCredentials.runIfTrue { + credentialsGetter() + } + } else { + stateGetter().get(it) + } + }) + return ConcurrentCrudable(crudableLists, SimpleReadWriteAdapter()) + } + + should("findAllZosmfExistingConnection") { + val configCollections: MutableMap> = mutableMapOf( + Pair(ConnectionConfig::class.java.name, mutableListOf(connection)), + Pair(FilesWorkingSetConfig::class.java.name, mutableListOf()), + Pair(JesWorkingSetConfig::class.java.name, mutableListOf()), + ) + val sandboxState = SandboxState(ConfigStateV2(configCollections)) + val crudable = org.zowe.explorer.config.makeCrudableWithoutListeners(true, + { sandboxState.credentials }) { sandboxState.configState } + + mockedZoweConfigService::class.declaredMemberProperties.find { it.name == "configCrudable" }?.let { + it.isAccessible = true + it.javaField?.set(mockedZoweConfigService, crudable) + } + + mockedZoweConfigService::class.declaredMemberFunctions.find { it.name == "findAllZosmfExistingConnection" }?.let { + it.isAccessible = true + (it.call(mockedZoweConfigService, ZoweConfigType.LOCAL) as List).size shouldBe 1 + it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL) shouldBe null + connection.name = "zowe-global-zosmf" + it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL) shouldBe null + connection.zoweConfigPath = getZoweConfigLocation(mockedProject, ZoweConfigType.GLOBAL) + (it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL) as List).size shouldBe 1 + } + } + + should("findExistingConnection") { + mockedZoweConfigService::class.declaredMemberFunctions.find { it.name == "findExistingConnection" }?.let { + it.isAccessible = true + connection.name = "zowe-local-zosmf/testProj" + connection.zoweConfigPath = getZoweConfigLocation(mockedProject, ZoweConfigType.LOCAL) + (it.call( + mockedZoweConfigService, + ZoweConfigType.LOCAL, + "zosmf" + ) as ConnectionConfig).name shouldBe connection.name + connection.name = "zowe-global-zosmf" + connection.zoweConfigPath = getZoweConfigLocation(mockedProject, ZoweConfigType.LOCAL) + it.call(mockedZoweConfigService, ZoweConfigType.LOCAL, "zosmf") shouldBe null + it.call(mockedZoweConfigService, ZoweConfigType.GLOBAL, "zosmf") shouldBe null + } + } + + should("validateForBlank") { + validateForBlank(JPasswordField())?.message shouldBe "This field must not be blank" + validateForBlank(JTextField())?.message shouldBe "This field must not be blank" + } + + should("getZoweConfigState NEED_TO_ADD") { + every { mockedZoweConfigService["findAllZosmfExistingConnection"](any()) } returns listOf( + connection + ) + every { zoweConfigMock.getListOfZosmfConections() } returns zosConnList1 + every { mockedZoweConfigService["findExistingConnection"](any(), any()) } returns null + mockedZoweConfigService.getZoweConfigState(false, ZoweConfigType.LOCAL) shouldBe ZoweConfigState.NEED_TO_ADD + val zossConn2 = ZOSConnection( + "222.222.222.222", + "10443", + "testUser", + "testPassword", + rejectUnauthorized = true, + basePath = "/base/config/path/", + profileName = "zosmf1" + ) + zosConnList1.clear() + zosConnList1.add(zossConn2) + connection.name = "zowe-global-lpar.zosmf" + every { + mockedZoweConfigService["findExistingConnection"]( + any(), any() + ) + } returns connection + mockedZoweConfigService.getZoweConfigState(false, ZoweConfigType.LOCAL) shouldBe ZoweConfigState.NEED_TO_ADD + } + } -}) +}) \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/explorer/config/connect/CredentialServiceTest.kt b/src/test/kotlin/org/zowe/explorer/config/connect/CredentialServiceTest.kt new file mode 100644 index 000000000..679930af1 --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/config/connect/CredentialServiceTest.kt @@ -0,0 +1,70 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.config.connect + +import com.intellij.credentialStore.CredentialAttributes +import com.intellij.credentialStore.Credentials +import com.intellij.ide.passwordSafe.PasswordSafe +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.zowe.explorer.testutils.WithApplicationShouldSpec + +class CredentialServiceTest : WithApplicationShouldSpec({ + + val credentialServiceMock = spyk(recordPrivateCalls = true) + var isNullSet = false + var isCredentionalsSet = false + + afterSpec { + clearAllMocks() + unmockkAll() + } + + beforeEach { + isNullSet = false + isCredentionalsSet = false + } + + context("CredentialServiceTest") { + val passSafeService = mockk() + every { credentialServiceMock["getPasswordSafeService"]() } returns passSafeService + every { passSafeService.get(any()) } answers { + if (firstArg().serviceName.endsWith("000UID")) null + else Credentials("user", "password") + } + every { passSafeService.set(any(), any()) } answers { + if (secondArg() == null) isNullSet = true + else isCredentionalsSet = true + } + + should("getUsernameByKey") { + credentialServiceMock.getUsernameByKey("000UID") shouldBe null + credentialServiceMock.getUsernameByKey("validUid") shouldBe "user" + } + + should("getPasswordByKey") { + credentialServiceMock.getPasswordByKey("000UID") shouldBe null + credentialServiceMock.getPasswordByKey("validUid") shouldBe "password" + } + + should("setCredentials") { + credentialServiceMock.setCredentials("validUid", "user", "password") + isNullSet shouldBe false + isCredentionalsSet shouldBe true + } + + should("clearCredentials") { + credentialServiceMock.clearCredentials("validUid") + isNullSet shouldBe true + isCredentionalsSet shouldBe false + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt new file mode 100644 index 000000000..291ddcf87 --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt @@ -0,0 +1,247 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.config.connect.ui.zosmf + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.showOkCancelDialog +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.mockk.* +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.kotlinsdk.zowe.config.DefaultKeytarWrapper +import org.zowe.kotlinsdk.zowe.config.KeytarWrapper +import org.zowe.kotlinsdk.zowe.config.ZoweConfig +import java.nio.file.Path +import javax.swing.Icon +import kotlin.reflect.KFunction +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.jvm.isAccessible + +class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ + + val zOSMFConnectionConfigurableMock = spyk() + var isShowOkCancelDialogCalled = false + var isFindFileByNioPathCalled = false + var isInputStreamCalled = false + + afterSpec { + clearAllMocks() + unmockkAll() + } + + beforeEach { + isShowOkCancelDialogCalled = false + isFindFileByNioPathCalled = false + isInputStreamCalled = false + } + + context("ZOSMFConnectionConfigurable:") { + + val state = ConnectionDialogState( + connectionUuid = "0000", + connectionUrl = "https://111.111.111.111:111", + connectionName = "zowe-local-zosmf/testProj", + zoweConfigPath = "/zowe/conf/path" + ) + + val ret = mutableListOf(Messages.OK) + val showOkCancelDialogMock: (String, String, String, String, Icon?, DialogWrapper.DoNotAskOption?, Project?) -> Int = + ::showOkCancelDialog + mockkStatic(showOkCancelDialogMock as KFunction<*>) + every { + showOkCancelDialogMock(any(), any(), any(), any(), null, null, null) + } answers { + isShowOkCancelDialogCalled = true + ret[0] + } + + mockkConstructor(DefaultKeytarWrapper::class) + every { anyConstructed().setPassword(any(), any(), any()) } just Runs + every { anyConstructed().deletePassword(any(), any()) } returns true + + should("updateZoweConfigIfNeeded null state and Ok") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + it.call(zOSMFConnectionConfigurableMock, null) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + should("updateZoweConfigIfNeeded null zoweConfigPath and Ok") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + state.zoweConfigPath = null + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + ret[0] = Messages.CANCEL + + should("updateZoweConfigIfNeeded null state and Cancel") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + state.zoweConfigPath = null + it.call(zOSMFConnectionConfigurableMock, null) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + state.zoweConfigPath = "/zowe/conf/path" + ret[0] = Messages.OK + + should("updateZoweConfigIfNeeded throw Zowe config file not found") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + val vfMock = mockk() + val vfmMock: VirtualFileManager = mockk() + mockkStatic(VirtualFileManager::class) + every { VirtualFileManager.getInstance() } returns vfmMock + every { vfmMock.findFileByNioPath(any()) } answers { + isFindFileByNioPathCalled = true + vfMock + } + every { vfMock.inputStream } answers { + isInputStreamCalled = true + val fileCont = "{\n" + + " \"\$schema\": \"./zowe.schema.json\",\n" + + " \"profiles\": {\n" + + " \"zosmf\": {\n" + + " \"type\": \"zosmf\",\n" + + " \"properties\": {\n" + + " \"port\": 443\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"tso\": {\n" + + " \"type\": \"tso\",\n" + + " \"properties\": {\n" + + " \"account\": \"\",\n" + + " \"codePage\": \"1047\",\n" + + " \"logonProcedure\": \"IZUFPROC\"\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"ssh\": {\n" + + " \"type\": \"ssh\",\n" + + " \"properties\": {\n" + + " \"port\": 22\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"base\": {\n" + + " \"type\": \"base\",\n" + + " \"properties\": {\n" + + " \"host\": \"example.host\",\n" + + " \"rejectUnauthorized\": true\n" + + " },\n" + + " \"secure\": [\n" + + " \"user\",\n" + + " \"password\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"defaults\": {\n" + + " \"zosmf\": \"zosmf\",\n" + + " \"tso\": \"tso\",\n" + + " \"ssh\": \"ssh\",\n" + + " \"base\": \"base\"\n" + + " }\n" + + "}" + fileCont.toByteArray().inputStream() + } + every { vfMock.path } returns "/zowe/file/path/zowe.config.json" + every { vfMock.charset } returns Charsets.UTF_8 + every { vfMock.setBinaryContent(any()) } just Runs + + mockkObject(ZoweConfig) + val confMap = mutableMapOf>() + val configCredentialsMap = mutableMapOf() + configCredentialsMap["profiles.base.properties.user"] = "testUser" + configCredentialsMap["profiles.base.properties.password"] = "testPass" + confMap.clear() + confMap["/zowe/file/path/zowe.config.json"] = configCredentialsMap + every { ZoweConfig.Companion["readZoweCredentialsFromStorage"](any()) } returns confMap + + should("updateZoweConfigIfNeeded success") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + state.connectionUrl = "https://testhost.com:10443" + it.call(zOSMFConnectionConfigurableMock, state) + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe true + isInputStreamCalled shouldBe true + } + + should("updateZoweConfigIfNeeded empty port") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + state.isAllowSsl = true + state.connectionUrl = "https://testhost.com" + it.call(zOSMFConnectionConfigurableMock, state) + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe true + isInputStreamCalled shouldBe true + } + + should("updateZoweConfigIfNeeded failed") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + state.connectionUrl = "https://111@@@:8080" + try { + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Unable to save invalid URL") + } + } + } + + } +} +) \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/explorer/zowe/ZoweStartupActivityTest.kt b/src/test/kotlin/org/zowe/explorer/zowe/ZoweStartupActivityTest.kt new file mode 100644 index 000000000..596203e79 --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/zowe/ZoweStartupActivityTest.kt @@ -0,0 +1,88 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.zowe + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.explorer.zowe.service.ZoweConfigService +import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl +import org.zowe.explorer.zowe.service.ZoweConfigState +import org.zowe.explorer.zowe.service.ZoweConfigType +import javax.swing.Icon +import kotlin.reflect.KFunction + + +class ZoweStartupActivityTest : WithApplicationShouldSpec({ + var isDialogCalled = false + var isConnDeleted = false + + afterSpec { + clearAllMocks() + unmockkAll() + } + + beforeEach { + isDialogCalled = false + isConnDeleted = false + } + + context("ZoweStartupActivity") { + val mockedProject = mockk(relaxed = true) + every { mockedProject.basePath } returns "test" + every { mockedProject.name } returns "testProj" + val mockedZoweConfigService = spyk(ZoweConfigServiceImpl(mockedProject), recordPrivateCalls = true) + every { mockedProject.service() } returns mockedZoweConfigService + every { mockedZoweConfigService.getZoweConfigState(type = any()) } returns ZoweConfigState.NEED_TO_ADD + every { mockedZoweConfigService.deleteZoweConfig(type = any()) } answers { + isConnDeleted = true + } + var ret = mutableListOf(0) + val showDialogRef: (Project, String, String, Array, Int, Icon) -> Int = Messages::showDialog + mockkStatic(showDialogRef as KFunction<*>) + every { + Messages.showDialog(any(), any(), any(), any>(), any(), any()) + } answers { + isDialogCalled = true + ret[0] + } + + should("showDialogForDeleteZoweConfigIfNeeded NEED_TO_ADD") { + showDialogForDeleteZoweConfigIfNeeded(mockedProject, ZoweConfigType.LOCAL) + isDialogCalled shouldBe false + } + + should("showDialogForDeleteZoweConfigIfNeeded NOT_EXISTS") { + every { mockedZoweConfigService.getZoweConfigState(type = any()) } returns ZoweConfigState.NOT_EXISTS + showDialogForDeleteZoweConfigIfNeeded(mockedProject, ZoweConfigType.LOCAL) + isDialogCalled shouldBe false + } + + should("showDialogForDeleteZoweConfigIfNeeded SYNCHRONIZED") { + every { mockedZoweConfigService.getZoweConfigState(type = any()) } returns ZoweConfigState.SYNCHRONIZED + showDialogForDeleteZoweConfigIfNeeded(mockedProject, ZoweConfigType.LOCAL) + isDialogCalled shouldBe true + isConnDeleted shouldBe true + } + + should("showDialogForDeleteZoweConfigIfNeeded SYNCHRONIZED Global Keep conn") { + ret[0] = 1 + showDialogForDeleteZoweConfigIfNeeded(mockedProject, ZoweConfigType.GLOBAL) + isDialogCalled shouldBe true + isConnDeleted shouldBe false + } + + } + +}) \ No newline at end of file