diff --git a/.github/workflows/IJ.yml b/.github/workflows/IJ.yml index 13d59c503..4aaf63fee 100644 --- a/.github/workflows/IJ.yml +++ b/.github/workflows/IJ.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - IJ: [IC-2021.1, IC-2021.2, IC-2021.3, IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1, IC-2023.2, IC-2023.3] + IJ: [IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1, IC-2023.2, IC-2023.3] steps: - uses: actions/checkout@v2 diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/SwingUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/SwingUtils.kt new file mode 100644 index 000000000..d1bbb6430 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/SwingUtils.kt @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes + +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import javax.swing.text.JTextComponent + +fun createExplanationLabel(text: String): JBLabel { + return JBLabel(text).apply { + componentStyle = UIUtil.ComponentStyle.SMALL + foreground = JBUI.CurrentTheme.Link.Foreground.DISABLED + } +} + +fun insertNewLineAtCaret(textComponent: JTextComponent) { + val caretPosition = textComponent.caretPosition + val newText = StringBuilder(textComponent.text).insert(caretPosition, '\n').toString() + textComponent.text = newText + textComponent.caretPosition = caretPosition + 1 +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt new file mode 100644 index 000000000..08da77f2a --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt @@ -0,0 +1,237 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.balloon + +import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.ExpirableRunnable +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI +import com.redhat.devtools.intellij.kubernetes.createExplanationLabel +import com.redhat.devtools.intellij.kubernetes.insertNewLineAtCaret +import java.awt.BorderLayout +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.KeyListener +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import java.util.function.Supplier +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.text.JTextComponent +import kotlin.math.max + +class StringInputBalloon( + private val value: String, + private val setValue: (String) -> Unit, + private val editor: Editor +) { + + private var isValid = false + + fun show(event: MouseEvent) { + val disposable = Disposer.newDisposable() + val panel = JPanel(BorderLayout()) + + val balloon = createBalloon(panel) + Disposer.register(balloon, disposable) + + val view = TextAreaView(value, balloon, disposable) + view.addTo(panel) + + balloon.addListener(view.onClosed()) + balloon.show(RelativePoint(event), Balloon.Position.above) + + val focusManager = IdeFocusManager.getInstance(editor.project) + focusManager.doWhenFocusSettlesDown(view.onFocusSettled(focusManager)) + } + + private fun createBalloon(panel: JPanel): Balloon { + return JBPopupFactory.getInstance() + .createBalloonBuilder(panel) + .setCloseButtonEnabled(true) + .setBlockClicksThroughBalloon(true) + .setAnimationCycle(0) + .setHideOnKeyOutside(true) + .setHideOnClickOutside(true) + .setFillColor(panel.background) + .setHideOnAction(false) // allow user to Ctrl+A & Ctrl+C + .createBalloon() + } + + private fun isMultiline(): Boolean { + return value.contains('\n') + } + + private fun setValue(balloon: Balloon, textComponent: JTextComponent): Boolean { + return if (isValid) { + balloon.hide() + setValue.invoke(textComponent.text) + true + } else { + false + } + } + + private inner class TextAreaView( + private val value: String, + private val balloon: Balloon, + private val disposable: Disposable + ) { + private val MIN_ROWS = 4 + private val MAX_COLUMNS = 64 + + private lateinit var textArea: JTextArea + private lateinit var applyButton: JButton + private lateinit var keyListener: KeyListener + + fun addTo(panel: JPanel) { + val label = JBLabel("Value:") + label.border = JBUI.Borders.empty(0, 3, 4, 0) + panel.add(label, BorderLayout.NORTH) + val textArea = JBTextArea( + value, + max(MIN_ROWS, value.length.floorDiv(MAX_COLUMNS) + 1), // textarea has text lines + 1 + MAX_COLUMNS - 1 + ) + textArea.lineWrap = !isMultiline() // have text area line wrap if content is not manually wrapped + textArea.wrapStyleWord = true + val scrolled = ScrollPaneFactory.createScrollPane(textArea, true) + panel.add(scrolled, BorderLayout.CENTER) + this.keyListener = onKeyPressed(textArea, balloon) + textArea.addKeyListener(keyListener) + this.textArea = textArea + + val buttonPanel = JPanel(BorderLayout()) + buttonPanel.border = JBUI.Borders.empty(2, 0, 0, 0) + panel.add(buttonPanel, BorderLayout.SOUTH) + buttonPanel.add( + createExplanationLabel("Shift & Return to insert a new line, Return to apply"), + BorderLayout.CENTER + ) + applyButton = JButton("Apply") + applyButton.addMouseListener(onApply(textArea, balloon)) + buttonPanel.add(applyButton, BorderLayout.EAST) + + addValidation(textArea, disposable) + } + + fun onFocusSettled(focusManager: IdeFocusManager): ExpirableRunnable { + return object : ExpirableRunnable { + + override fun run() { + focusManager.requestFocus(textArea, true) + textArea.selectAll() + } + + override fun isExpired(): Boolean { + return false + } + } + } + + private fun addValidation(textComponent: JTextComponent, disposable: Disposable) { + ComponentValidator(disposable) + .withValidator(ValueValidator(textComponent)) + .installOn(textComponent) + .andRegisterOnDocumentListener(textComponent) + .revalidate() + } + + private fun onApply(textComponent: JTextComponent, balloon: Balloon): MouseListener { + return object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent?) { + setValue(balloon, textComponent) + } + } + } + + private fun onKeyPressed(textComponent: JTextComponent, balloon: Balloon): KeyListener { + return object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + when { + KeyEvent.VK_ESCAPE == e.keyCode -> + balloon.hide() + + KeyEvent.VK_ENTER == e.keyCode + && (e.isShiftDown || e.isControlDown) -> { + insertNewLineAtCaret(textComponent) + } + + KeyEvent.VK_ENTER == e.keyCode + && (!e.isShiftDown && !e.isControlDown) -> + if (setValue(balloon, textComponent)) { + e.consume() + } + } + } + } + } + + fun onClosed(): JBPopupListener { + return object : JBPopupListener { + override fun beforeShown(event: LightweightWindowEvent) { + // do nothing + } + + override fun onClosed(event: LightweightWindowEvent) { + dispose() + } + } + } + + private fun dispose() { + textArea.removeKeyListener(keyListener) + } + + private inner class ValueValidator(private val textComponent: JTextComponent) : Supplier { + + override fun get(): ValidationInfo? { + if (!textComponent.isEnabled + || !textComponent.isVisible + ) { + return null + } + return validate(textComponent.text) + } + + private fun validate(newValue: String): ValidationInfo? { + val validation = when { + StringUtil.isEmptyOrSpaces(newValue) -> + ValidationInfo("Provide a value", textComponent).asWarning() + + value == newValue -> + ValidationInfo("Provide new value", textComponent).asWarning() + + else -> + null + } + this@StringInputBalloon.isValid = (validation == null) + applyButton.setEnabled(validation == null) + return validation + } + } + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64Presentations.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64Presentations.kt new file mode 100644 index 000000000..4f39f8bb0 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64Presentations.kt @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +@file:Suppress("UnstableApiUsage") +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.presentation.InlayPresentation +import com.intellij.codeInsight.hints.presentation.PresentationFactory +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon +import com.redhat.devtools.intellij.kubernetes.editor.util.getBinaryData +import com.redhat.devtools.intellij.kubernetes.editor.util.getData +import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource +import com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis +import org.jetbrains.concurrency.runAsync +import java.awt.event.MouseEvent + +/** + * A factory that creates an [InlayPresentationsFactory] that creates an [InlayPresentationsFactory] for the given kubernetes resource. + * The [InlayPresentationsFactory] creates [InlayPresentation]s for the properties in the resource. + * + * @see [create] + */ +object Base64Presentations { + + private const val SECRET_RESOURCE_KIND = "Secret" + private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap" + + fun create(content: PsiElement, info: KubernetesResourceInfo, sink: InlayHintsSink, editor: Editor): InlayPresentationsFactory? { + return when { + isKubernetesResource(SECRET_RESOURCE_KIND, info) -> { + val data = getData(content) ?: return null + StringPresentationsFactory(data, sink, editor) + } + + isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> { + val binaryData = getBinaryData(content) ?: return null + BinaryPresentationsFactory(binaryData, sink, editor) + } + + else -> null + } + } + + abstract class InlayPresentationsFactory( + private val element: PsiElement, + protected val sink: InlayHintsSink, + protected val editor: Editor + ) { + + protected companion object { + const val INLAY_HINT_MAX_WIDTH = 50 + const val WRAP_BASE64_STRING_AT = 76 + } + + fun create(): Collection { + return element.children.mapNotNull { child -> + val adapter = Base64ValueAdapter(child) + create(adapter) + } + } + + protected abstract fun create(adapter: Base64ValueAdapter): InlayPresentation? + + } + + class StringPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor) + : InlayPresentationsFactory(element, sink, editor) { + + override fun create(adapter: Base64ValueAdapter): InlayPresentation? { + val decoded = adapter.getDecoded() ?: return null + val offset = adapter.getStartOffset() ?: return null + val onClick = StringInputBalloon( + decoded, + onValidValue(adapter::set, editor.project), + editor + )::show + val presentation = create(decoded, onClick, editor) ?: return null + sink.addInlineElement(offset, false, presentation, false) + return presentation + } + + private fun create(text: String, onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? { + val factory = PresentationFactory(editor as EditorImpl) + val trimmed = trimWithEllipsis(text, INLAY_HINT_MAX_WIDTH) ?: return null + val textPresentation = factory.smallText(trimmed) + val hoverPresentation = factory.referenceOnHover(textPresentation) { event, _ -> + onClick.invoke(event) + } + val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation) + return factory.roundWithBackground(tooltipPresentation) + } + + private fun onValidValue(setter: (value: String, wrapAt: Int) -> Unit, project: Project?) + : (value: String) -> Unit { + return { value -> + runAsync { + WriteCommandAction.runWriteCommandAction(project) { + setter.invoke(value, WRAP_BASE64_STRING_AT) + } + } + } + } + + } + + class BinaryPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor) + : InlayPresentationsFactory(element, sink, editor) { + + override fun create(adapter: Base64ValueAdapter): InlayPresentation? { + val decoded = adapter.getDecodedBytes() ?: return null + val offset = adapter.getStartOffset() ?: return null + val presentation = create(decoded, editor) ?: return null + sink.addInlineElement(offset, false, presentation, false) + return presentation + } + + private fun create(bytes: ByteArray, editor: Editor): InlayPresentation? { + val factory = PresentationFactory(editor as EditorImpl) + val hex = toHexString(bytes) ?: return null + val trimmed = trimWithEllipsis(hex, INLAY_HINT_MAX_WIDTH) ?: return null + return factory.roundWithBackground(factory.smallText(trimmed)) + } + + private fun toHexString(bytes: ByteArray): String? { + return try { + bytes.joinToString(separator = " ") { byte -> + Integer.toHexString(byte.toInt()) + } + } catch (e: Exception) { + null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt new file mode 100644 index 000000000..964a33876 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.psi.PsiElement +import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64 +import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64ToBytes +import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64 +import com.redhat.devtools.intellij.kubernetes.editor.util.getValue +import com.redhat.devtools.intellij.kubernetes.editor.util.setValue + +class Base64ValueAdapter(private val element: PsiElement) { + + private companion object { + private val CONTENT_REGEX = Regex("[^\"\n |]*", RegexOption.MULTILINE) + private const val START_MULTILINE = "|\n" + private const val QUOTE = "\"" + } + + fun set(value: String, wrapAt: Int = -1) { + val possiblyMultiline = if (isMultiline()) { + wrap(wrapAt, START_MULTILINE + encodeBase64(value)) + } else { + encodeBase64(value) + } + ?: return + val possiblyQuoted = + if (isQuoted()) { + QUOTE + possiblyMultiline + QUOTE + } else { + possiblyMultiline + } + setValue(possiblyQuoted, element) + } + + private fun wrap(at: Int, string: String?): String? { + return when { + string == null -> null + at == -1 -> string + else -> { + string.chunked(at).joinToString("\n") + } + } + } + + fun get(): String? { + return getValue(element) + } + + private fun isMultiline(): Boolean { + return get()?.startsWith(START_MULTILINE) ?: false + } + + private fun isQuoted(): Boolean { + val value = get() ?: return false + return value.startsWith(QUOTE) + && value.endsWith(QUOTE) + } + + fun getDecoded(): String? { + val value = get() ?: return null + val content = CONTENT_REGEX + .findAll(value) + .filter { matchResult -> matchResult.value.isNotBlank() } + .map { matchResult -> matchResult.value } + .joinToString(separator = "") + return decodeBase64(content) + } + + fun getDecodedBytes(): ByteArray? { + return decodeBase64ToBytes(get()) + } + + fun getStartOffset(): Int? { + return com.redhat.devtools.intellij.kubernetes.editor.util.getStartOffset(element) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt new file mode 100644 index 000000000..8f255d28c --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +@file:Suppress("UnstableApiUsage") + +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.codeInsight.hints.ChangeListener +import com.intellij.codeInsight.hints.FactoryInlayHintsCollector +import com.intellij.codeInsight.hints.ImmediateConfigurable +import com.intellij.codeInsight.hints.InlayHintsCollector +import com.intellij.codeInsight.hints.InlayHintsProvider +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.NoSettings +import com.intellij.codeInsight.hints.SettingsKey +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.ui.dsl.builder.panel +import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getContent +import javax.swing.JComponent + + +internal class Base64ValueInlayHintsProvider : InlayHintsProvider { + + override val key: SettingsKey = SettingsKey("KubernetesResource.hints") + override val name: String = "Kubernetes" + override val previewText: String = "Preview" + + override fun createSettings(): NoSettings { + return NoSettings() + } + + override fun createConfigurable(settings: NoSettings): ImmediateConfigurable { + return object : ImmediateConfigurable { + override fun createComponent(listener: ChangeListener): JComponent = panel {} + + override val mainCheckboxText: String = "Show hints for:" + + override val cases: List = emptyList() + } + } + + override fun getCollectorFor(file: PsiFile, editor: Editor, settings: NoSettings, sink: InlayHintsSink): InlayHintsCollector? { + val info = KubernetesResourceInfo.extractMeta(file) ?: return null + return Collector(editor, info) + } + + private class Collector(editor: Editor, private val info: KubernetesResourceInfo) : FactoryInlayHintsCollector(editor) { + + override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { + if (element !is PsiFile + || !element.isValid) { + return true + } + val content = getContent(element) ?: return true + val factory = Base64Presentations.create(content, info, sink, editor) ?: return true + factory.create() ?: return true + return false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt index 10ca4d707..b3b4ca6f2 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt @@ -10,6 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.editor.util +import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonElementGenerator import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonProperty @@ -17,19 +18,25 @@ import com.intellij.json.psi.JsonValue import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.Strings import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager +import com.intellij.refactoring.suggested.startOffset import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import org.jetbrains.yaml.YAMLElementGenerator import org.jetbrains.yaml.YAMLUtil import org.jetbrains.yaml.psi.YAMLFile import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLPsiElement import org.jetbrains.yaml.psi.YAMLValue +import java.util.Base64 private const val KEY_METADATA = "metadata" +private const val KEY_DATA = "data" +private const val KEY_BINARY_DATA = "binaryData" private const val KEY_RESOURCE_VERSION = "resourceVersion" /** @@ -59,6 +66,11 @@ fun isKubernetesResource(resourceInfo: KubernetesResourceInfo?): Boolean { && resourceInfo?.typeInfo?.kind?.isNotBlank() ?: false } +fun isKubernetesResource(kind: String, resourceInfo: KubernetesResourceInfo?): Boolean { + return resourceInfo?.typeInfo?.apiGroup?.isNotBlank() ?: false + && kind == resourceInfo?.typeInfo?.kind +} + /** * Returns [KubernetesResourceInfo] for the given file and project. Returns `null` if it could not be retrieved. * @@ -134,10 +146,18 @@ private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: YAM } } +fun getContent(element: PsiElement): PsiElement? { + if (element !is PsiFile) { + return null + } + return getContent(element) +} + private fun getContent(file: PsiFile): PsiElement? { return when (file) { is YAMLFile -> { - if (file.documents.isEmpty()) { + if (file.documents == null + || file.documents.isEmpty()) { return null } file.documents[0].topLevelValue @@ -154,32 +174,178 @@ fun getMetadata(document: Document?, psi: PsiDocumentManager): PsiElement? { } val file = psi.getPsiFile(document) ?: return null val content = getContent(file) ?: return null - return getMetadata(content) ?: return null + return getMetadata(content) } +/** + * Returns the [PsiElement] named "metadata" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "metadata" child should be found. + * @return the PsiElement named "metadata" + */ private fun getMetadata(content: PsiElement): PsiElement? { return when (content) { is YAMLValue -> content.children - .filterIsInstance(YAMLKeyValue::class.java) + .filterIsInstance() .find { it.name == KEY_METADATA } is JsonValue -> content.children.toList() - .filterIsInstance(JsonProperty::class.java) + .filterIsInstance() .find { it.name == KEY_METADATA } else -> null } } +/** + * Returns the [PsiElement] named "data" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "data" child should be found. + * @return the PsiElement named "data" + */ +fun getData(element: PsiElement): PsiElement? { + return when (element) { + is YAMLPsiElement -> + element.children + .filterIsInstance() + .find { it.name == KEY_DATA } + ?.value + is JsonElement -> + element.children.toList() + .filterIsInstance() + .find { it.name == KEY_DATA } + ?.value + else -> + null + } +} + +/** + * Returns the [PsiElement] named "binaryData" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "binaryData" child should be found. + * @return the PsiElement named "binaryData" + */ +fun getBinaryData(element: PsiElement): PsiElement? { + return when (element) { + is YAMLPsiElement -> + element.children + .filterIsInstance() + .find { it.name == KEY_BINARY_DATA } + ?.value + is JsonElement -> + element.children.toList() + .filterIsInstance() + .find { it.name == KEY_BINARY_DATA } + ?.value + else -> + null + } +} + +/** + * Returns a base64 decoded String for the given base64 encoded String. + * Returns `null` if decoding fails. + * + * @param value the string to be decoded + * @return a decoded String for the given base64 encoded String. + */ +fun decodeBase64(value: String?): String? { + val bytes = decodeBase64ToBytes(value) ?: return null + return String(bytes) +} + +/** + * Returns base64 decoded bytes for the given base64 encoded string. + * Returns `null` if decoding fails. + * + * @param value the string to be decoded + * @return decoded bytes for the given base64 encoded string. + */ +fun decodeBase64ToBytes(value: String?): ByteArray? { + if (Strings.isEmptyOrSpaces(value)) { + return value?.toByteArray() + } + return try { + Base64.getDecoder().decode(value) + } catch (e: IllegalArgumentException) { + null + } +} + +/** + * Returns the base64 encoded string of the given string. + * Returns `null` if encoding fails. + * + * @param value the string to be encoded + * @return the base64 encoded string for the given string. + */ +fun encodeBase64(value: String): String? { + if (Strings.isEmptyOrSpaces(value)) { + return value + } + return try { + val bytes = Base64.getEncoder().encode(value.toByteArray()) + String(bytes) + } catch (e: IllegalArgumentException) { + null + } +} + +/** + * Returns the String value of the given [YAMLKeyValue] or [JsonProperty]. + * + * @param element the psi element to retrieve the startOffset from + * @return the startOffset in the value of the given psi element + */ +fun getValue(element: PsiElement): String? { + return when (element) { + is YAMLKeyValue -> element.value?.text + is JsonProperty -> element.value?.text + else -> null + } +} + +/** + * Returns the startOffset in the [YAMLValue] or [JsonValue] of the given [PsiElement]. + * Returns `null` otherwise. + * + * @param element the psi element to retrieve the startOffset from + * @return the startOffset in the value of the given psi element + */ +fun getStartOffset(element: PsiElement): Int? { + return when (element) { + is YAMLKeyValue -> element.value?.startOffset + is JsonProperty -> element.value?.startOffset + else -> null + } +} + +fun setValue(value: String, element: PsiElement) { + val newElement = when (element) { + is YAMLKeyValue -> + YAMLElementGenerator.getInstance(element.project).createYamlKeyValue(element.keyText, value) + is JsonProperty -> + JsonElementGenerator(element.project).createProperty(element.name, value) + else -> + null + } ?: return + element.parent.addAfter(newElement, element) + element.delete() +} + private fun getResourceVersion(metadata: YAMLKeyValue): YAMLKeyValue? { return metadata.value?.children - ?.filterIsInstance(YAMLKeyValue::class.java) + ?.filterIsInstance() ?.find { it.name == KEY_RESOURCE_VERSION } } private fun getResourceVersion(metadata: JsonProperty): JsonProperty? { return metadata.value?.children?.toList() - ?.filterIsInstance(JsonProperty::class.java) + ?.filterIsInstance() ?.find { it.name == KEY_RESOURCE_VERSION } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f04365da9..91bcc976b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -206,6 +206,9 @@ + diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt new file mode 100644 index 000000000..14cf3ecb7 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.nhaarman.mockitokotlin2.mock +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLValue +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesTypeInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + + +class Base64PresentationsTest { + + private val secret = kubernetesResourceInfo( + "yoda", "light side", kubernetesTypeInfo("Secret", "v1") + ) + private val configMap = kubernetesResourceInfo( + "skywalker", "light side", kubernetesTypeInfo("ConfigMap", "v1") + ) + private val pod = kubernetesResourceInfo( + "anakin", "dark side", kubernetesTypeInfo("Pod", "v1") + ) + + private val dataElement = createYAMLKeyValue("data") + private val binaryDataElement = createYAMLKeyValue("binaryData") + + @Test + fun `#create should create factory for Secret if has data`() { + // given + val content = createYAMLValue(arrayOf(dataElement)) + // when + val factory = Base64Presentations.create(content, secret, mock(), mock()) + // then + assertThat(factory).isNotNull() + } + + @Test + fun `#create should NOT create factory for Secret if has NO data`() { + // given + val content = createYAMLValue(emptyArray()) + // when + val factory = Base64Presentations.create(content, secret, mock(), mock()) + // then + assertThat(factory).isNull() + } + + @Test + fun `#create should create factory for ConfigMap if has binaryData`() { + // given + val content = createYAMLValue(arrayOf(binaryDataElement)) + // when + val factory = Base64Presentations.create(content, configMap, mock(), mock()) + // then + assertThat(factory).isNotNull() + } + + @Test + fun `#create NOT should create factory for ConfigMap if has NO binaryData`() { + // given + val content = createYAMLValue(emptyArray()) + // when + val factory = Base64Presentations.create(content, configMap, mock(), mock()) + // then + assertThat(factory).isNull() + } + + @Test + fun `#create should NOT create factory for Pod`() { + // given + // when + val factory = Base64Presentations.create(mock(), pod, mock(), mock()) + // then + assertThat(factory).isNull() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt new file mode 100644 index 000000000..24bec832b --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt @@ -0,0 +1,228 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.json.psi.JsonProperty +import com.intellij.psi.PsiElement +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonProperty +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonPsiFileFactory +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createProjectWithServices +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLGenerator +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.junit.Test +import java.util.Base64 + +class Base64ValueAdapterTest { + + private val yamlElementGenerator = yamlElementGenerator() + private val project = createProjectWithServices(yamlElementGenerator) + + private fun yamlElementGenerator() = createYAMLGenerator() + + @Test + fun `#get should return value of YAMLKeyValue`() { + // given + val element = createYAMLKeyValue(value = "yoda", project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.get() + // then + assertThat(text).isEqualTo("yoda") + } + + @Test + fun `#get should return value with quotes`() { + // given + val element = createYAMLKeyValue(value = "\"yoda\"", project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.get() + // then + assertThat(text).isEqualTo("\"yoda\"") + } + + @Test + fun `#get should return value of JsonProperty`() { + // given + val element = createJsonProperty(value = "yoda", project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.get() + // then + assertThat(text).isEqualTo("yoda") + } + + @Test + fun `#get should return null for unknown PsiElement`() { + // given + val element = mock() + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.get() + // then + assertThat(text).isNull() + } + + @Test + fun `#getDecoded should return value decoded value`() { + // given + val element = createYAMLKeyValue(value = toBase64("skywalker"), project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.getDecoded() + // then + assertThat(text).isEqualTo("skywalker") + } + + @Test + fun `#getDecoded should return null if value isn't valid base64`() { + // given + val element = createYAMLKeyValue(value = toBase64("skywalker") + "bogus", project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.getDecoded() + // then + assertThat(text).isNull() + } + + @Test + fun `#getDecoded should return null if value is null`() { + // given + val element = createYAMLKeyValue(value = null, project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.getDecoded() + // then + assertThat(text).isNull() + } + + @Test + fun `#getDecoded should return value without quotes`() { + // given + val element = createYAMLKeyValue(value = "\"" + toBase64("yoda") + "\"", project = project) + val adapter = Base64ValueAdapter(element) + // when + val text = adapter.getDecoded() + // then + assertThat(text).isEqualTo("yoda") + } + + @Test + fun `#getDecodedBytes should return decoded bytes`() { + // given + val element = createYAMLKeyValue(value = toBase64("skywalker"), project = project) + val adapter = Base64ValueAdapter(element) + // when + val bytes = adapter.getDecodedBytes() + // then + assertThat(bytes).isEqualTo("skywalker".toByteArray()) + } + + @Test + fun `#getDecodedBytes should return null for null value`() { + // given + val element = createYAMLKeyValue(value = null, project = project) + val adapter = Base64ValueAdapter(element) + // when + val bytes = adapter.getDecodedBytes() + // then + assertThat(bytes).isNull() + } + + @Test + fun `#set should add new YAMKeyValue to parent and delete current element`() { + // given + val parent = createYAMLKeyValue("group", "jedis", project = project) + val element = createYAMLKeyValue("jedi", "yoda", parent, project) + val adapter = Base64ValueAdapter(element) + // when + adapter.set("luke") + // then + verify(parent).addAfter(any(), eq(element)) + verify(element).delete() + } + + @Test + fun `#set should create new YAMKeyValue with same key and given base64 encoded value`() { + // given + val parent = createYAMLKeyValue("group", "jedis", project = project) + val element = createYAMLKeyValue("jedi", "yoda", parent, project) + val adapter = Base64ValueAdapter(element) + // when + adapter.set("obiwan") + // then + verify(yamlElementGenerator).createYamlKeyValue("jedi", toBase64("obiwan")) + } + + @Test + fun `#set should create new multiline value if existing value is multiline`() { + // given + val parent = createYAMLKeyValue("group", "jedis", project = project) + val element = createYAMLKeyValue("jedi", "|\nyoda", parent, project) + val adapter = Base64ValueAdapter(element) + // when + adapter.set("obiwan") + // then + verify(yamlElementGenerator).createYamlKeyValue(any(), eq("|\n" + toBase64("obiwan"))) + } + + @Test + fun `#set should create new quoted value if existing value is quoted`() { + // given + val parent = createYAMLKeyValue("group", "jedis", project = project) + val element = createYAMLKeyValue("jedi", "\"yoda\"", parent, project) + val adapter = Base64ValueAdapter(element) + // when + adapter.set("anakin") + // then + verify(yamlElementGenerator).createYamlKeyValue(any(), eq("\"" + toBase64("anakin") + "\"")) + } + + @Test + fun `#set should wrap new value at given position`() { + // given + val parent = createYAMLKeyValue("group", "jedis", project = project) + val element = createYAMLKeyValue("jedi", "yoda", parent, project) + val adapter = Base64ValueAdapter(element) + // when + adapter.set("|\njedi qui-gon", 4) + // then + verify(yamlElementGenerator).createYamlKeyValue(any(), + eq(toBase64("|\njedi qui-gon").chunked(4).joinToString(""))) + } + + @Test + fun `#set should add new JsonProperty to parent and delete current element`() { + // given + val properties: MutableList = mutableListOf() + val psiFileFactory = createJsonPsiFileFactory(properties) + val project = createProjectWithServices(psiFileFactory = psiFileFactory) + val parent = createJsonProperty("group", "jedis", project = project) + val property = createJsonProperty("jedi", "yoda", parent, project) + properties.add(property) + val adapter = Base64ValueAdapter(property) + // when + adapter.set("luke") + // then + verify(parent).addAfter(any(), eq(property)) + verify(property).delete() + } + + private fun toBase64(value: String): String { + return String(Base64.getEncoder().encode(value.toByteArray())) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt new file mode 100644 index 000000000..b84bb38f0 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.mocks + +import com.intellij.json.psi.JsonObject +import com.intellij.json.psi.JsonProperty +import com.intellij.json.psi.JsonValue +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doAnswer +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import org.jetbrains.yaml.YAMLElementGenerator +import org.jetbrains.yaml.psi.YAMLDocument +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLPsiElement +import org.jetbrains.yaml.psi.YAMLValue +import kotlin.random.Random + +fun createYAMLKeyValue( + key: String = Random.nextInt().toString(), + value: String? = null, + parent: YAMLKeyValue? = null, + project: Project = mock() +): YAMLKeyValue { + val valueElement: YAMLValue = mock { + on { getText() } doReturn value + } + return mock { + on { getName() } doReturn key + on { getKeyText() } doReturn key + on { getValue() } doReturn valueElement + on { getParent() } doReturn parent + on { getProject() } doReturn project + } +} + +fun createJsonProperty( + name: String = Random.nextInt().toString(), + value: String? = null, + parent: JsonProperty? = null, + project: Project +): JsonProperty { + val valueElement: JsonValue = mock { + on { getText() } doReturn value + } + return mock { + on { getValue() } doReturn valueElement + on { getName() } doReturn name + on { getValue() } doReturn valueElement + on { getParent() } doReturn parent + on { getProject() } doReturn project + } +} + +fun createProjectWithServices( + yamlGenerator: YAMLElementGenerator? = null, + psiFileFactory: PsiFileFactory? = null +): Project { + return mock { + on { getService(any>()) } doAnswer { invocation -> + when { + YAMLElementGenerator::class.java == invocation.getArgument>(0) -> + yamlGenerator + + PsiFileFactory::class.java == invocation.getArgument>(0) -> + psiFileFactory + + else -> null + } + } + } +} + +fun createYAMLFile(documents: List?): YAMLFile { + return mock { + on { getDocuments() } doReturn documents + } +} + +fun createYAMLValue(children: Array): YAMLValue { + return mock { + on { getChildren() } doReturn children + } +} + +fun createYAMLDocument(yamlValue: YAMLValue): YAMLDocument { + return mock { + on { getTopLevelValue() } doReturn yamlValue + } +} + + +fun createJsonPsiFile(properties: List): PsiFile { + val firstChild: JsonObject = mock { + on { getPropertyList() } doReturn properties + } + return mock { + on { getFirstChild() } doReturn firstChild + } +} + +fun createJsonPsiFileFactory(properties: List): PsiFileFactory { + val file = createJsonPsiFile(properties) + return createPsiFileFactory(file) +} + +fun createPsiFileFactory(psiFile: PsiFile): PsiFileFactory { + return mock { + on { createFileFromText(any(), any(), any()) } doReturn psiFile + } +} + +fun createYAMLGenerator(): YAMLElementGenerator { + return mock { + on { createYamlKeyValue(any(), any()) } doReturn mock() + } +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt new file mode 100644 index 000000000..05bf5e1ea --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.util + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLDocument +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLFile +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLValue +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesTypeInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.util.Base64 + +class ResourceEditorUtilsTest { + + @Test + fun `#isKubernetesResource should return true for info with apiGroup and kind`() { + // given + val kubernetesTypeInfo: KubernetesTypeInfo = + kubernetesTypeInfo("jedi", "v1") + val kubernetesResourceInfo: KubernetesResourceInfo = + kubernetesResourceInfo("yoda", "light side", kubernetesTypeInfo) + // when + val isKubernetesResource = isKubernetesResource(kubernetesResourceInfo) + // then + assertThat(isKubernetesResource).isTrue() + } + + @Test + fun `#isKubernetesResource should return false for info without apiGroup`() { + // given + val kubernetesTypeInfo: KubernetesTypeInfo = + kubernetesTypeInfo("jedi", null) + val kubernetesResourceInfo: KubernetesResourceInfo = mock() + kubernetesResourceInfo("yoda", "light side", kubernetesTypeInfo) + // when + val isKubernetesResource = isKubernetesResource(kubernetesResourceInfo) + // then + assertThat(isKubernetesResource).isFalse() + } + + @Test + fun `#isKubernetesResource should return false for info without kind`() { + // given + val kubernetesTypeInfo: KubernetesTypeInfo = + kubernetesTypeInfo(null, "v1") + val kubernetesResourceInfo: KubernetesResourceInfo = mock() + kubernetesResourceInfo("yoda", "light side", kubernetesTypeInfo) + // when + val isKubernetesResource = isKubernetesResource(kubernetesResourceInfo) + // then + assertThat(isKubernetesResource).isFalse() + } + + @Test + fun `#isKubernetesResource should return true if info has the given kind`() { + // given + val kubernetesTypeInfo: KubernetesTypeInfo = + kubernetesTypeInfo("jedi", "v1") + val kubernetesResourceInfo: KubernetesResourceInfo = + kubernetesResourceInfo("yoda", "light side", kubernetesTypeInfo) + // when + val isKubernetesResource = isKubernetesResource("jedi", kubernetesResourceInfo) + // then + assertThat(isKubernetesResource).isTrue() + } + + @Test + fun `#isKubernetesResource should return false if info doesnt have the given kind`() { + // given + val kubernetesTypeInfo: KubernetesTypeInfo = + kubernetesTypeInfo("jedi", "v1") + val kubernetesResourceInfo: KubernetesResourceInfo = + kubernetesResourceInfo("yoda", "light side", kubernetesTypeInfo) + // when + val isKubernetesResource = isKubernetesResource("sith", kubernetesResourceInfo) + // then + assertThat(isKubernetesResource).isFalse() + } + + @Test + fun `#getContent should return content in YAMLFile`() { + // given + val value = createYAMLValue(emptyArray()) + val document = createYAMLDocument(value) + val file = createYAMLFile(listOf(document)) + // when + getContent(file) + // then + verify(file.documents.get(0)).getTopLevelValue() + } + + @Test + fun `#getContent should return null if YAMLFile has empty list of documents`() { + // given + val file = createYAMLFile(emptyList()) + // when + val content = getContent(file) + // then + assertThat(content).isNull() + } + + @Test + fun `#getContent should return null if YAMLFile has null documents`() { + // given + val file = createYAMLFile(null) + // when + val content = getContent(file) + // then + assertThat(content).isNull() + } + + @Test + fun `#getData should return YAMLKeyValue named data`() { + // given + val data = createYAMLKeyValue("data") + val parent = createYAMLValue(arrayOf(data)) + // when + val found = getData(parent) + // then + assertThat(found).isNotNull() + } + + @Test + fun `#getData should return null if there is no child named data`() { + // given + val yoda = createYAMLKeyValue("yoda") + val parent = createYAMLValue(arrayOf(yoda)) + // when + val found = getData(parent) + // then + assertThat(found).isNull() + } + + @Test + fun `#getBinaryData should return YAMLKeyValue named binaryData`() { + // given + val data = createYAMLKeyValue("binaryData") + val parent = createYAMLValue(arrayOf(data)) + // when + val found = getBinaryData(parent) + // then + assertThat(found).isNotNull() + } + + @Test + fun `#getBinaryData should return null if there is no child named binaryData`() { + // given + val anakin = createYAMLKeyValue("anakin") + val parent = createYAMLValue(arrayOf(anakin)) + // when + val found = getBinaryData(parent) + // then + assertThat(found).isNull() + } + + @Test + fun `#decodeBase64 should return base64 decoded value for given string`() { + // given + val encoded = String(Base64.getEncoder().encode("anakin".toByteArray())) + // when + val decoded = decodeBase64(encoded) + // then + assertThat(decoded).isEqualTo("anakin") + } + + @Test + fun `#decodeBase64 should return null if given string is NOT base64 encoded`() { + // given + val encoded = String(Base64.getEncoder().encode("anakin".toByteArray())) + "bogus" + // when + val decoded = decodeBase64(encoded) + // then + assertThat(decoded).isNull() + } + + @Test + fun `#decodeBase64 should return null if given string is null`() { + // given + // when + val decoded = decodeBase64(null) + // then + assertThat(decoded).isNull() + } + + @Test + fun `#decodeBase64 should return blank if given string is blank`() { + // given + // when + val decoded = decodeBase64("") + // then + assertThat(decoded).isEqualTo("") + } +} \ No newline at end of file