From e2df3dd1dd5496fb1d5e751304543d6ec2e9e42b Mon Sep 17 00:00:00 2001 From: Saeed Rezaee Date: Thu, 15 Feb 2024 15:33:15 +0100 Subject: [PATCH] Add tests for Hawkbit's downloadOnly feature This commit includes tests for the download-only feature in Hawkbit. It also introduces an abstract class, AbstractDeploymentTest, to offer a structured approach for testing deployments and streamline development. Signed-off-by: Saeed Rezaee --- .../integrationtest/AbstractDeploymentTest.kt | 197 ++++++++++++++++++ .../HawkbitDownloadOnlyDeploymentTest.kt | 109 ++++++++++ .../ddiclient/integrationtest/utils/Utils.kt | 34 ++- 3 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/AbstractDeploymentTest.kt create mode 100644 src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/HawkbitDownloadOnlyDeploymentTest.kt diff --git a/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/AbstractDeploymentTest.kt b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/AbstractDeploymentTest.kt new file mode 100644 index 0000000..41d395d --- /dev/null +++ b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/AbstractDeploymentTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright © 2017-2024 Kynetics LLC + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hara.ddiclient.integrationtest + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.eclipse.hara.ddiclient.api.ConfigDataProvider +import org.eclipse.hara.ddiclient.api.DeploymentPermitProvider +import org.eclipse.hara.ddiclient.api.DirectoryForArtifactsProvider +import org.eclipse.hara.ddiclient.api.DownloadBehavior +import org.eclipse.hara.ddiclient.api.HaraClient +import org.eclipse.hara.ddiclient.api.HaraClientData +import org.eclipse.hara.ddiclient.api.HaraClientDefaultImpl +import org.eclipse.hara.ddiclient.api.MessageListener +import org.eclipse.hara.ddiclient.api.Updater +import org.eclipse.hara.ddiclient.integrationtest.api.management.Action +import org.eclipse.hara.ddiclient.integrationtest.api.management.HawkbitAssignDistributionBody +import org.eclipse.hara.ddiclient.integrationtest.api.management.HawkbitTargetInfo +import org.eclipse.hara.ddiclient.integrationtest.api.management.ManagementApi +import org.eclipse.hara.ddiclient.integrationtest.api.management.ManagementClient +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils +import org.eclipse.hara.ddiclient.integrationtest.utils.addOkhttpLogger +import org.eclipse.hara.ddiclient.integrationtest.utils.internalLog +import org.testng.Assert +import org.testng.annotations.AfterTest +import org.testng.annotations.BeforeTest +import java.io.File +import java.lang.Exception +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds + +abstract class AbstractDeploymentTest { + + protected lateinit var managementApi: ManagementApi + abstract val targetId: String + protected var actionId: Int = 0 + + private var assertServerActionsScope = CoroutineScope(Dispatchers.IO) + private var assertServerActionsOnCompleteJob: Deferred? = null + private var testScope = CoroutineScope(Dispatchers.Default) + + private val throwableScope = CoroutineScope(Dispatchers.Default) + private var throwableJob: Deferred? = null + + private val eventListener = object : MessageListener { + override fun onMessage(message: MessageListener.Message) { + "Message received: $message".internalLog() + when (message) { + + is MessageListener.Message.Event.UpdateFinished, + MessageListener.Message.State.CancellingUpdate -> { + testScope.launch { + try { + assertServerActionsOnCompleteJob?.await() + } catch (ignored: CancellationException) { + } + } + } + + else -> { + } + } + } + } + + @BeforeTest + open fun beforeTest() { + managementApi = ManagementClient.newInstance(TestUtils.hawkbitUrl) + } + + @AfterTest + open fun afterTest() { + } + + protected fun defaultClientFromTargetId( + directoryDataProvider: DirectoryForArtifactsProvider = TestUtils.directoryDataProvider, + configDataProvider: ConfigDataProvider = TestUtils.configDataProvider, + updater: Updater = TestUtils.updater, + messageListeners: List = listOf(eventListener), + deploymentPermitProvider: DeploymentPermitProvider = object : + DeploymentPermitProvider {}, + downloadBehavior: DownloadBehavior = TestUtils.downloadBehavior, + okHttpClientBuilder: OkHttpClient.Builder = OkHttpClient.Builder().addOkhttpLogger(), + targetToken: String? = "", + gatewayToken: String? = TestUtils.gatewayToken + ): (String) -> HaraClient = { targetId -> + val clientData = HaraClientData( + TestUtils.tenantName, + targetId, + TestUtils.hawkbitUrl, + gatewayToken, + targetToken) + + val client = HaraClientDefaultImpl() + + client.init( + clientData, + directoryDataProvider, + configDataProvider, + deploymentPermitProvider, + listOf(*messageListeners.toTypedArray()), + listOf(updater), + downloadBehavior, + httpBuilder = okHttpClientBuilder + ) + client + } + + protected suspend fun reCreateTestTargetOnServer() { + runCatching { + managementApi.deleteTarget(TestUtils.basic, targetId) + } + runCatching { + managementApi.createTarget( + TestUtils.basic, listOf(HawkbitTargetInfo(targetId))) + } + } + + protected suspend fun assignDistributionToTheTarget( + distribution: HawkbitAssignDistributionBody) { + val response = + managementApi.assignDistributionToTarget(TestUtils.basic, + targetId, distribution) + if (response.assignedActions.isNotEmpty()) { + actionId = response.assignedActions.first().id + } + } + + protected suspend fun startTheTestAndWaitForResult(client: HaraClient, + deployment: TestUtils.TargetDeployments) { + client.startAsync() + assertServerActionsOnCompleteJob = assertServerActionsOnComplete(client, deployment) + + testScope.async { + while (assertServerActionsOnCompleteJob?.isCompleted == false) { + delay(1.seconds) + } + }.await() + } + + private fun assertServerActionsOnComplete( + client: HaraClient, + deployment: TestUtils.TargetDeployments): Deferred { + return assertServerActionsScope.async(start = CoroutineStart.LAZY) { + val deploymentInfo = deployment.deploymentInfo.first() + while (managementApi.getActionAsync(TestUtils.basic, deployment.targetId, + deploymentInfo.actionId).status != Action.Status.finished + ) { + delay(5.seconds) + } + + val actionStatus = + managementApi.getTargetActionStatusAsync(TestUtils.basic, deployment.targetId, + deploymentInfo.actionId) + assertEquals(actionStatus.content, + deploymentInfo.actionStatusOnFinish.content) + + deploymentInfo.filesDownloadedPairedWithServerFile.forEach { (fileDownloaded, serverFile) -> + assertEquals(File(fileDownloaded).readText(), + File(serverFile).readText()) + File(fileDownloaded).deleteRecursively() + } + + client.safeStopClient() + } + } + + private suspend fun assertEquals(actual: Any?, expected: Any?) { + throwableJob = throwableScope.async { + Assert.assertEquals(actual, expected) + } + try { + throwableJob?.await() + } catch (ignored: CancellationException) { + } + } + + private fun HaraClient.safeStopClient() { + try { + stop() + } catch (ignored: Exception) { + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/HawkbitDownloadOnlyDeploymentTest.kt b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/HawkbitDownloadOnlyDeploymentTest.kt new file mode 100644 index 0000000..fc90d5b --- /dev/null +++ b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/HawkbitDownloadOnlyDeploymentTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright © 2017-2024 Kynetics LLC + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hara.ddiclient.integrationtest + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.eclipse.hara.ddiclient.api.DeploymentPermitProvider +import org.eclipse.hara.ddiclient.api.HaraClient +import org.eclipse.hara.ddiclient.integrationtest.api.management.ActionStatus +import org.eclipse.hara.ddiclient.integrationtest.api.management.AssignDistributionType +import org.eclipse.hara.ddiclient.integrationtest.api.management.HawkbitAssignDistributionBody +import org.eclipse.hara.ddiclient.integrationtest.api.management.ServerSystemConfig +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils.endMessagesOnSuccessUpdate +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils.messagesOnSoftUpdateAuthorization +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils.messagesOnSuccessfullyDownloadDistribution +import org.eclipse.hara.ddiclient.integrationtest.utils.TestUtils.targetRetrievedUpdateAction +import org.testng.annotations.BeforeTest +import org.testng.annotations.Test + +class HawkbitDownloadOnlyDeploymentTest : AbstractDeploymentTest() { + + override val targetId: String = "DownloadOnlyTest" + + companion object { + const val DISTRIBUTION_ID = 3 + } + + @BeforeTest + override fun beforeTest() { + super.beforeTest() + setPollingTime() + } + + private fun setPollingTime() = runBlocking { + managementApi.setPollingTime(TestUtils.basic, ServerSystemConfig("00:00:05")) + } + + @Test(enabled = true, timeOut = 60_000) + fun testDownloadOnlyWhileWaitingForUpdateAuthorization() = runBlocking { + + reCreateTestTargetOnServer() + + assignDownloadOnlyDistribution() + + val client = createClient() + + startTheTestAndWaitForResult(client, + createTargetTestDeployment(getActionsOnFinishForForceUpdateScenario)) + } + + private suspend fun assignDownloadOnlyDistribution() { + val distribution = HawkbitAssignDistributionBody(DISTRIBUTION_ID, + AssignDistributionType.DOWNLOAD_ONLY, 0) + assignDistributionToTheTarget(distribution) + } + + private fun createClient(): HaraClient { + + val deploymentBehavior = object : DeploymentPermitProvider { + override fun downloadAllowed() = CompletableDeferred(false) + override fun updateAllowed() = CompletableDeferred(true) + } + return defaultClientFromTargetId( + deploymentPermitProvider = deploymentBehavior) + .invoke(targetId) + } + + private fun createTargetTestDeployment( + actionsOnFinish: ActionStatus): TestUtils.TargetDeployments { + val filesDownloadedPairedToServerFile = setOf( + TestUtils.pathResolver.fromArtifact(actionId.toString()).invoke( + TestUtils.test1Artifact) to TestUtils.locationOfFileNamed("test1")) + + return TestUtils.TargetDeployments( + targetId = targetId, + targetToken = "", + deploymentInfo = listOf( + TestUtils.TargetDeployments.DeploymentInfo( + actionId = actionId, + actionStatusOnStart = ActionStatus( + setOf( + TestUtils.firstActionWithAssignmentEntry + )), + actionStatusOnFinish = actionsOnFinish, + filesDownloadedPairedWithServerFile = filesDownloadedPairedToServerFile + ) + ) + ) + } + + private val getActionsOnFinishForForceUpdateScenario : ActionStatus = + ActionStatus(setOf( + *endMessagesOnSuccessUpdate, + *messagesOnSoftUpdateAuthorization, + *messagesOnSuccessfullyDownloadDistribution( + TestUtils.md5OfFileNamed("test1"), targetId, + "1", "test_1"), + targetRetrievedUpdateAction, + TestUtils.firstActionWithAssignmentEntry + )) +} \ No newline at end of file diff --git a/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/utils/Utils.kt b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/utils/Utils.kt index 2219c80..3cd4f98 100644 --- a/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/utils/Utils.kt +++ b/src/test/kotlin/org/eclipse/hara/ddiclient/integrationtest/utils/Utils.kt @@ -276,11 +276,13 @@ object TestUtils { listOf("Assignment initiated by user 'test'") ) + val targetRetrievedUpdateAction = ActionStatus.ContentEntry( + ActionStatus.ContentEntry.Type.retrieved, + listOf("Update Server: Target retrieved update action and should start now the download.") + ) + val startMessagesOnUpdateFond = arrayOf( - ActionStatus.ContentEntry( - ActionStatus.ContentEntry.Type.retrieved, - listOf("Update Server: Target retrieved update action and should start now the download.") - ), + targetRetrievedUpdateAction, firstActionEntry ) @@ -301,6 +303,30 @@ object TestUtils { val defaultActionStatusOnStart = ActionStatus(setOf(firstActionEntry)) + + fun messagesOnSuccessfullyDownloadDistribution( + md5: String, targetId: String, softwareModuleId: String, + fileName: String) = arrayOf( + ActionStatus.ContentEntry( + ActionStatus.ContentEntry.Type.running, + listOf("Successfully downloaded all files") + ), + ActionStatus.ContentEntry( + ActionStatus.ContentEntry.Type.running, + listOf( + "Successfully downloaded file with md5 $md5" + ) + ), + ActionStatus.ContentEntry( + ActionStatus.ContentEntry.Type.download, + listOf( + "Update Server: Target downloads /$tenantNameToLower/controller/v1/$targetId/softwaremodules/$softwareModuleId/artifacts/$fileName") + ), + ActionStatus.ContentEntry( + ActionStatus.ContentEntry.Type.running, + listOf("Start downloading 1 files") + ) + ) }