diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt index ba86e69c..5d2b968c 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt @@ -35,7 +35,15 @@ import org.springframework.kafka.test.utils.KafkaTestUtils import org.springframework.test.annotation.DirtiesContext @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@EmbeddedKafka(topics = ["\${kafka.producers.command-feedback.topic}"]) +@EmbeddedKafka( + topics = + [ + "\${kafka.consumers.command.topic}", + "\${kafka.consumers.pre-shared-key.topic}", + "\${kafka.producers.command-feedback.topic}", + "\${kafka.producers.device-message.topic}" + ] +) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class CoapMessageHandlingTest { companion object { diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/entity/Command.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/entity/Command.kt index 7be6bc14..16d1e6a5 100644 --- a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/entity/Command.kt +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/entity/Command.kt @@ -41,23 +41,13 @@ class Command( fun cancel() = apply { status = CommandStatus.CANCELLED } - enum class CommandType( - val downlink: String, - val urcsSuccess: List, - val urcsError: List, - val needsCommandValue: Boolean = false - ) { - PSK("PSK", listOf("PSK:TMP"), listOf("PSK:DLER", "PSK:HSER")), - PSK_SET("PSK:SET", listOf("PSK:SET"), listOf("PSK:DLER", "PSK:HSER", "PSK:EQER")), - REBOOT("CMD:REBOOT", listOf("INIT", "WDR"), listOf()), - RSP("CMD:RSP", listOf("CMD:RSP"), listOf("DLER")), - RSP2("CMD:RSP2", listOf("CMD:RSP2"), listOf("DLER")), - FIRMWARE( - "OTA", - listOf("OTA:SUC"), - listOf("OTA:CSER", "OTA:HSER", "OTA:RST", "OTA:SWNA", "OTA:FLER"), - needsCommandValue = true - ), + enum class CommandType(val downlink: String, val needsCommandValue: Boolean = false) { + PSK("PSK"), + PSK_SET("PSK:SET"), + REBOOT("CMD:REBOOT"), + RSP("CMD:RSP"), + RSP2("CMD:RSP2"), + FIRMWARE("OTA", needsCommandValue = true), } enum class CommandStatus { diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/exception/NoCommandResultHandlerForCommandTypeException.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/exception/NoCommandResultHandlerForCommandTypeException.kt new file mode 100644 index 00000000..dac34e00 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/exception/NoCommandResultHandlerForCommandTypeException.kt @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.exception + +class NoCommandResultHandlerForCommandTypeException(message: String) : Exception(message) diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandler.kt new file mode 100644 index 00000000..86ed6a0a --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandler.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.gxf.crestdeviceservice.model.ErrorUrc.Companion.getMessageFromCode + +abstract class CommandResultHandler( + private val commandService: CommandService, + private val commandFeedbackService: CommandFeedbackService +) { + private val logger = KotlinLogging.logger {} + + abstract val supportedCommandType: CommandType + + abstract fun hasSucceeded(deviceId: String, body: JsonNode): Boolean + + abstract fun hasFailed(deviceId: String, body: JsonNode): Boolean + + fun handleSuccess(command: Command) { + logger.info { "Command ${command.type} succeeded for device with id ${command.deviceId}." } + + handleCommandSpecificSuccess(command) + + logger.debug { "Saving command and sending feedback to Maki." } + val successfulCommand = commandService.saveCommand(command.finish()) + commandFeedbackService.sendSuccessFeedback(successfulCommand) + } + + /** Override this method when custom success actions are needed. */ + open fun handleCommandSpecificSuccess(command: Command) { + logger.debug { + "Command ${command.type} for device with id ${command.deviceId} does not require specific success handling." + } + } + + fun handleFailure(command: Command, body: JsonNode) { + logger.info { "Command ${command.type} failed for device with id ${command.deviceId}." } + + handleCommandSpecificFailure(command, body) + + val failedCommand = commandService.saveCommand(command.fail()) + val errorMessages = body.urcs().joinToString(". ") { urc -> getMessageFromCode(urc) } + commandFeedbackService.sendErrorFeedback(failedCommand, "Command failed. Error(s): $errorMessages.") + } + + /** Override this method when command specific failure actions are needed */ + open fun handleCommandSpecificFailure(command: Command, body: JsonNode) { + logger.debug { + "Command ${command.type} for device with id ${command.deviceId} does not require specific failure handling." + } + } + + fun handleStillInProgress(command: Command) { + logger.info { "Command ${command.type} still in progress for device with id ${command.deviceId}." } + } + + companion object { + private const val URC_FIELD = "URC" + private const val DL_FIELD = "DL" + + fun JsonNode.urcs(): List = this[URC_FIELD].filter { it.isTextual }.map { it.asText() } + + fun JsonNode.downlinks(): List = + this[URC_FIELD].first { it.isObject }[DL_FIELD].asText().replace("!", "").split(";") + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandlerConfig.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandlerConfig.kt new file mode 100644 index 00000000..2814e7e5 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/CommandResultHandlerConfig.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class CommandResultHandlerConfig { + + @Bean + fun commandResultHandlersByType(commandResultHandlers: List) = + commandResultHandlers.associateBy { it.supportedCommandType } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandler.kt new file mode 100644 index 00000000..d79a65d2 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandler.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.springframework.stereotype.Component + +@Component +class FirmwareCommandResultHandler( + val commandService: CommandService, + val commandFeedbackService: CommandFeedbackService +) : CommandResultHandler(commandService, commandFeedbackService) { + + private val succesUrc = "OTA:SUC" + private val errorUrcs = listOf("OTA:CSER", "OTA:HSER", "OTA:RST", "OTA:SWNA", "OTA:FLER") + + override val supportedCommandType = CommandType.FIRMWARE + + override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs() + + override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandler.kt new file mode 100644 index 00000000..2504518e --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandler.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.springframework.stereotype.Component + +@Component +class PskCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) : + CommandResultHandler(commandService, commandFeedbackService) { + + private val succesUrc = "PSK:TMP" + private val errorUrcs = listOf("PSK:DLER", "PSK:HSER") + + override val supportedCommandType = CommandType.PSK + + override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs() + + override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandler.kt new file mode 100644 index 00000000..9137bf0b --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandler.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.gxf.crestdeviceservice.psk.service.PskService +import org.springframework.stereotype.Component + +@Component +class PskSetCommandResultHandler( + val pskService: PskService, + val commandService: CommandService, + val commandFeedbackService: CommandFeedbackService +) : CommandResultHandler(commandService, commandFeedbackService) { + + private val logger = KotlinLogging.logger {} + + private val succesUrc = "PSK:SET" + private val errorUrcs = listOf("PSK:DLER", "PSK:HSER", "PSK:EQER") + + override val supportedCommandType = CommandType.PSK_SET + + override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs() + + override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs } + + override fun handleCommandSpecificSuccess(command: Command) { + logger.info { "PSK SET command succeeded: Changing active key for device ${command.deviceId}" } + pskService.changeActiveKey(command.deviceId) + } + + override fun handleCommandSpecificFailure(command: Command, body: JsonNode) { + logger.info { "PSK SET command failed: Setting pending key as invalid for device ${command.deviceId}" } + pskService.setPendingKeyAsInvalid(command.deviceId) + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandler.kt new file mode 100644 index 00000000..66c008ee --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandler.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.springframework.stereotype.Component + +@Component +class RebootCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) : + CommandResultHandler(commandService, commandFeedbackService) { + + private val succesUrcs = listOf("INIT", "WDR") + + override val supportedCommandType = CommandType.REBOOT + + override fun hasSucceeded(deviceId: String, body: JsonNode) = body.urcs().containsAll(succesUrcs) + + override fun hasFailed(deviceId: String, body: JsonNode) = false +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandler.kt new file mode 100644 index 00000000..d9074434 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandler.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.springframework.stereotype.Component + +@Component +class Rsp2CommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) : + RspCommandResultHandler(commandService, commandFeedbackService) { + + override val confirmationDownlinkInUrc = "CMD:RSP2" + override val errorUrc = "RSP2:DLER" + + override val supportedCommandType = CommandType.RSP2 +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandler.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandler.kt new file mode 100644 index 00000000..5ffee70e --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandler.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import com.fasterxml.jackson.databind.JsonNode +import org.gxf.crestdeviceservice.command.entity.Command.CommandType +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.springframework.stereotype.Component + +@Component +class RspCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) : + CommandResultHandler(commandService, commandFeedbackService) { + + val confirmationDownlinkInUrc = "CMD:RSP" + val errorUrc = "RSP:DLER" + + override val supportedCommandType = CommandType.RSP + + override fun hasSucceeded(deviceId: String, body: JsonNode) = + confirmationDownlinkInUrc in body.downlinks() && errorUrc !in body.urcs() + + override fun hasFailed(deviceId: String, body: JsonNode) = errorUrc in body.urcs() +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultService.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultService.kt new file mode 100644 index 00000000..9d300b94 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultService.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.service + +import com.fasterxml.jackson.databind.JsonNode +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.exception.NoCommandResultHandlerForCommandTypeException +import org.gxf.crestdeviceservice.command.resulthandler.CommandResultHandler +import org.springframework.stereotype.Service + +@Service +class CommandResultService( + private val commandService: CommandService, + private val commandResultHandlersByType: Map +) { + private val logger = KotlinLogging.logger {} + + fun handleMessage(deviceId: String, body: JsonNode) { + val commandsInProgress = commandService.getAllCommandsInProgressForDevice(deviceId) + + commandsInProgress.forEach { checkResult(it, body) } + } + + private fun checkResult(command: Command, body: JsonNode) { + logger.debug { "Checking result for pending command of type ${command.type} for device ${command.deviceId}" } + + val resultHandler = + commandResultHandlersByType[command.type] + ?: throw NoCommandResultHandlerForCommandTypeException( + "No command result handler for command type ${command.type}" + ) + + when { + resultHandler.hasSucceeded(command.deviceId, body) -> resultHandler.handleSuccess(command) + resultHandler.hasFailed(command.deviceId, body) -> resultHandler.handleFailure(command, body) + else -> resultHandler.handleStillInProgress(command) + } + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/PayloadService.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/PayloadService.kt index 94e7ee64..181cead8 100644 --- a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/PayloadService.kt +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/PayloadService.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode import org.gxf.crestdeviceservice.command.entity.Command import org.gxf.crestdeviceservice.command.exception.NoMatchingCommandException import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandResultService import org.gxf.crestdeviceservice.command.service.CommandService import org.gxf.crestdeviceservice.firmware.service.FirmwareService import org.gxf.crestdeviceservice.model.DeviceMessage @@ -15,14 +16,14 @@ import org.springframework.stereotype.Service @Service class PayloadService( - private val urcService: UrcService, + private val commandResultService: CommandResultService, private val firmwareService: FirmwareService, private val commandService: CommandService, private val commandFeedbackService: CommandFeedbackService, ) { /** * Process the payload. This includes - * - checking URCs and updating the corresponding Commands + * - checking the payload for results for the commands sent via downlinks * - checking the payload for FMC (request for new FOTA packet) * * @param identity The identity of the device @@ -30,7 +31,7 @@ class PayloadService( * @param downlink The downlink to be returned to the device, fill it here if needed */ fun processPayload(identity: String, body: JsonNode, downlink: Downlink) { - urcService.interpretUrcsInMessage(identity, body) + commandResultService.handleMessage(identity, body) val deviceMessage = DeviceMessage(body) diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt deleted file mode 100644 index 9a493dcb..00000000 --- a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.crestdeviceservice.service - -import com.fasterxml.jackson.databind.JsonNode -import io.github.oshai.kotlinlogging.KotlinLogging -import org.gxf.crestdeviceservice.command.entity.Command -import org.gxf.crestdeviceservice.command.exception.NoMatchingCommandException -import org.gxf.crestdeviceservice.command.service.CommandFeedbackService -import org.gxf.crestdeviceservice.command.service.CommandService -import org.gxf.crestdeviceservice.model.Downlink -import org.gxf.crestdeviceservice.model.ErrorUrc.Companion.getMessageFromCode -import org.gxf.crestdeviceservice.psk.exception.NoExistingPskException -import org.gxf.crestdeviceservice.psk.service.PskService -import org.springframework.stereotype.Service - -@Service -class UrcService( - private val pskService: PskService, - private val commandService: CommandService, - private val commandFeedbackService: CommandFeedbackService -) { - companion object { - private const val URC_FIELD = "URC" - private const val DL_FIELD = "DL" - } - - private val logger = KotlinLogging.logger {} - - fun interpretUrcsInMessage(deviceId: String, body: JsonNode) { - val urcs = getUrcsFromMessage(body) - if (urcs.isEmpty()) { - logger.debug { "Received message without urcs" } - } else { - logger.debug { "Received message with urcs ${urcs.joinToString(", ")}" } - } - - getDownlinksFromMessage(body) - .filter { downlink -> downlink != Downlink.RESPONSE_SUCCESS && downlink.isNotBlank() } - .forEach { downlink -> handleDownlinkFromMessage(deviceId, downlink, urcs) } - } - - private fun getUrcsFromMessage(body: JsonNode) = body[URC_FIELD].filter { it.isTextual }.map { it.asText() } - - private fun getDownlinksFromMessage(body: JsonNode) = - body[URC_FIELD].first { it.isObject }[DL_FIELD].asText().split(";") - - private fun handleDownlinkFromMessage(deviceId: String, downlink: String, urcs: List) { - val command = getCommandThatDownlinkIsAbout(deviceId, downlink) - - if (command != null) { - handleUrcsForCommand(urcs, command, downlink) - } else { - throw NoMatchingCommandException( - "Message received with downlink: $downlink, but there is no matching command in progress in the database." - ) - } - } - - private fun getCommandThatDownlinkIsAbout(deviceId: String, downlink: String): Command? { - val commandsInProgress = commandService.getAllCommandsInProgressForDevice(deviceId) - return commandsInProgress.firstOrNull { command -> downlinkConcernsCommandType(downlink, command.type) } - } - - private fun downlinkConcernsCommandType(downlink: String, commandType: Command.CommandType): Boolean { - return if (commandType == Command.CommandType.PSK) { - // do not treat PSK_SET downlink as PSK command - !downlinkMatchesForCommandType(downlink, Command.CommandType.PSK_SET) - } else { - downlinkMatchesForCommandType(downlink, commandType) - } - } - - private fun downlinkMatchesForCommandType(downlink: String, commandType: Command.CommandType): Boolean { - val parts = commandType.downlink.split(":") - return parts.all { part -> downlink.contains(part) } - } - - private fun handleUrcsForCommand(urcs: List, command: Command, downlink: String) { - when { - urcsContainErrorsForCommand(urcs, command) -> handleCommandErrors(command, urcs) - urcsContainSuccessesForCommand(urcs, command) -> handleCommandSuccesses(command) - else -> - logger.warn { - "No urcs received for command '${command.type}' that was sent in downlink: $downlink. Urcs received: ${ - urcs.joinToString() - }." - } - } - } - - private fun urcsContainErrorsForCommand(urcs: List, command: Command) = - command.type.urcsError.any { errorUrc -> urcs.contains(errorUrc) } - - private fun urcsContainSuccessesForCommand(urcs: List, command: Command) = - command.type.urcsSuccess.any { successUrc -> urcs.contains(successUrc) } - - private fun handleCommandErrors(command: Command, urcs: List) { - if (command.type == Command.CommandType.PSK_SET) { - handlePskErrors(command.deviceId) - } - val errorMessages = urcs.joinToString(". ") { urc -> getMessageFromCode(urc) } - - logger.error { - "Command ${command.type} failed for device with id ${command.deviceId}. Error(s): $errorMessages." - } - - val failedCommand = commandService.saveCommand(command.fail()) - commandFeedbackService.sendErrorFeedback(failedCommand, "Command failed. Error(s): $errorMessages.") - } - - private fun handlePskErrors(deviceId: String) { - if (!pskService.isPendingPskPresent(deviceId)) { - throw NoExistingPskException("Failure URC received, but no pending key present to set as invalid") - } - pskService.setPendingKeyAsInvalid(deviceId) - } - - private fun handleCommandSuccesses(command: Command) { - if (command.type == Command.CommandType.PSK_SET) { - handlePskSetSuccess(command) - } - logger.info { - "Command ${command.type} for device ${command.deviceId} handled successfully. Saving command and sending feedback to Maki." - } - - val successfulCommand = commandService.saveCommand(command.finish()) - commandFeedbackService.sendSuccessFeedback(successfulCommand) - } - - private fun handlePskSetSuccess(command: Command) { - val deviceId = command.deviceId - if (!pskService.isPendingPskPresent(deviceId)) { - throw NoExistingPskException("Success URC received, but no pending key present to set as active") - } - logger.info { "PSK set successfully, changing active key" } - pskService.changeActiveKey(deviceId) - } -} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/CommandFactory.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/CommandFactory.kt index 8061a760..e3407df5 100644 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/CommandFactory.kt +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/CommandFactory.kt @@ -66,4 +66,26 @@ object CommandFactory { commandValue = "the-firmware-to-install", status = Command.CommandStatus.IN_PROGRESS ) + + fun rspCommandInProgress() = + Command( + id = UUID.randomUUID(), + deviceId = DEVICE_ID, + correlationId = CORRELATION_ID, + timestampIssued = timestamp, + type = Command.CommandType.RSP, + commandValue = null, + status = Command.CommandStatus.IN_PROGRESS + ) + + fun rsp2CommandInProgress() = + Command( + id = UUID.randomUUID(), + deviceId = DEVICE_ID, + correlationId = CORRELATION_ID, + timestampIssued = timestamp, + type = Command.CommandType.RSP2, + commandValue = null, + status = Command.CommandStatus.IN_PROGRESS + ) } diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/MessageFactory.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/MessageFactory.kt new file mode 100644 index 00000000..749e4404 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/MessageFactory.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import org.springframework.util.ResourceUtils + +object MessageFactory { + private const val URC_FIELD = "URC" + private const val DL_FIELD = "DL" + + private val mapper = ObjectMapper() + + fun messageTemplate(): ObjectNode { + val messageFile = ResourceUtils.getFile("classpath:message-template.json") + return mapper.readTree(messageFile) as ObjectNode + } + + fun messageWithUrc(urcs: List, downlink: String): JsonNode { + val urcNodes = urcs.map { urc -> TextNode(urc) } + val downlinkNode = ObjectNode(JsonNodeFactory.instance, mapOf(DL_FIELD to TextNode(downlink))) + + val urcFieldValue: ArrayNode = ObjectMapper().valueToTree(urcNodes + listOf(downlinkNode)) + + val message = messageTemplate() + message.replace(URC_FIELD, urcFieldValue) + + return message + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/TestHelper.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/TestHelper.kt deleted file mode 100644 index 53e80b64..00000000 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/TestHelper.kt +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.crestdeviceservice - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import org.springframework.util.ResourceUtils - -object TestHelper { - private val mapper = ObjectMapper() - - fun messageTemplate(): ObjectNode { - val messageFile = ResourceUtils.getFile("classpath:message-template.json") - return mapper.readTree(messageFile) as ObjectNode - } -} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandlerTest.kt new file mode 100644 index 00000000..1eec03a5 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/FirmwareCommandResultHandlerTest.kt @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class FirmwareCommandResultHandlerTest { + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var firmwareCommandResultHandler: FirmwareCommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.firmwareCommandInProgress() + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + firmwareCommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @Test + fun handleFailure() { + val command = CommandFactory.firmwareCommandInProgress() + val message = MessageFactory.messageWithUrc(listOf("OTA:HSER"), "") + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } + + firmwareCommandResultHandler.handleFailure(command, message) + + assertThat(command.status).isEqualTo(Command.CommandStatus.ERROR) + verify { commandService.saveCommand(command) } + verify { + commandFeedbackService.sendErrorFeedback(command, match { error -> error.contains("SHA256 hash error") }) + } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = firmwareCommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = firmwareCommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf("OTA:SUC"), "0", true), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("INIT", "WDR", "OTA:SUC"), "0", true), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("OTA:SUC"), "0", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("OTA:CSER"), "0", true), + Arguments.of(listOf("OTA:HSER"), "0", true), + Arguments.of(listOf("OTA:RST"), "0", true), + Arguments.of(listOf("OTA:SWNA"), "0", true), + Arguments.of(listOf("OTA:FLER"), "0", true), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandlerTest.kt new file mode 100644 index 00000000..a28342df --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskCommandResultHandlerTest.kt @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class PskCommandResultHandlerTest { + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var pskCommandResultHandler: PskCommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.pskCommandInProgress() + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + pskCommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @Test + fun handleFailure() { + val command = CommandFactory.pskCommandInProgress() + val message = MessageFactory.messageWithUrc(listOf("PSK:HSER"), "") + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } + + pskCommandResultHandler.handleFailure(command, message) + + assertThat(command.status).isEqualTo(Command.CommandStatus.ERROR) + verify { commandService.saveCommand(command) } + verify { + commandFeedbackService.sendErrorFeedback(command, match { error -> error.contains("SHA256 hash error") }) + } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = pskCommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = pskCommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf("PSK:TMP"), "0", true), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", true), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:SET"), "0", false), + Arguments.of(listOf("PSK:TMP", "PSK:SET"), "0", true), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("PSK:DLER"), "0", true), + Arguments.of(listOf("PSK:DLER"), "!PSK:#####", true), + Arguments.of(listOf("PSK:HSER"), "0", true), + Arguments.of(listOf("PSK:EQER"), "0", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("PSK:SET"), "!PSK:#####SET", false), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandlerTest.kt new file mode 100644 index 00000000..447146dd --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/PskSetCommandResultHandlerTest.kt @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.gxf.crestdeviceservice.psk.service.PskService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class PskSetCommandResultHandlerTest { + @MockK private lateinit var pskService: PskService + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var pskSetCommandResultHandler: PskSetCommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.pskSetCommandInProgress() + justRun { pskService.changeActiveKey(command.deviceId) } + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + pskSetCommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @Test + fun handleFailure() { + val command = CommandFactory.pskSetCommandInProgress() + val message = MessageFactory.messageWithUrc(listOf("PSK:EQER"), "") + justRun { pskService.setPendingKeyAsInvalid(command.deviceId) } + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } + + pskSetCommandResultHandler.handleFailure(command, message) + + assertThat(command.status).isEqualTo(Command.CommandStatus.ERROR) + verify { commandService.saveCommand(command) } + verify { + commandFeedbackService.sendErrorFeedback( + command, + match { error -> error.contains("Set PSK does not equal earlier PSK") } + ) + } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = pskSetCommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = pskSetCommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf("PSK:TMP"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:SET"), "0", true), + Arguments.of(listOf("PSK:TMP", "PSK:SET"), "!PSK:#####;PSK:#####SET", true), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("PSK:DLER"), "0", true), + Arguments.of(listOf("PSK:DLER"), "!PSK:#####", true), + Arguments.of(listOf("PSK:HSER"), "0", true), + Arguments.of(listOf("PSK:EQER"), "0", true), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("PSK:SET"), "!PSK:#####SET", false), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandlerTest.kt new file mode 100644 index 00000000..76302588 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RebootCommandResultHandlerTest.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class RebootCommandResultHandlerTest { + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var rebootCommandResultHandler: RebootCommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.rebootCommandInProgress() + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + rebootCommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = rebootCommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = rebootCommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf("INIT", "WDR"), "0", true), + Arguments.of(listOf("INIT"), "0", false), + Arguments.of(listOf("WDR"), "0", false), + Arguments.of(listOf("PSK:TMP", "PSK:SET"), "!PSK:######;PSK:######SET", false), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("INIT"), "0", false), + Arguments.of(listOf("WDR"), "0", false), + Arguments.of(listOf("PSK:DLER"), "!PSK:#####", false), + Arguments.of(listOf("PSK:HSER"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("PSK:SET"), "!PSK:#####SET", false), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandlerTest.kt new file mode 100644 index 00000000..7ba41b49 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/Rsp2CommandResultHandlerTest.kt @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class Rsp2CommandResultHandlerTest { + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var rsp2CommandResultHandler: Rsp2CommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.rsp2CommandInProgress() + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + rsp2CommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @Test + fun handleFailure() { + val command = CommandFactory.rsp2CommandInProgress() + val message = MessageFactory.messageWithUrc(listOf("PSK:HSER"), "") + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } + + rsp2CommandResultHandler.handleFailure(command, message) + + assertThat(command.status).isEqualTo(Command.CommandStatus.ERROR) + verify { commandService.saveCommand(command) } + verify { + commandFeedbackService.sendErrorFeedback(command, match { error -> error.contains("SHA256 hash error") }) + } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = rsp2CommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = rsp2CommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf(), "CMD:RSP2", true), + Arguments.of(listOf("PSK:TMP"), "!PSK:######;CMD:RSP2", true), + Arguments.of(listOf("PSK:TMP"), "!CMD:RSP2;PSK:######", true), + Arguments.of(listOf("RSP2:DLER"), "CMD:RSP2", false), + Arguments.of(listOf("RSP2:DLER", "PSK:TMP"), "!CMD:RSP2;PSK:######", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:SET"), "0", false), + Arguments.of(listOf("PSK:TMP", "PSK:SET"), "0", false), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("RSP2:DLER"), "CMD:RSP2", true), + Arguments.of(listOf("RSP2:DLER"), "CMD:RSP2", true), + Arguments.of(listOf("RSP2:DLER", "PSK:TMP"), "!CMD:RSP2;PSK:######", true), + Arguments.of(listOf(), "CMD:RSP2", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######;CMD:RSP2", false), + Arguments.of(listOf("PSK:TMP"), "!CMD:RSP2;PSK:######", false), + Arguments.of(listOf("PSK:DLER"), "!PSK:#####", false), + Arguments.of(listOf("PSK:HSER"), "0", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("PSK:SET"), "!PSK:#####SET", false), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandlerTest.kt new file mode 100644 index 00000000..071e34a1 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/resulthandler/RspCommandResultHandlerTest.kt @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.resulthandler + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import java.util.stream.Stream +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(MockKExtension::class) +class RspCommandResultHandlerTest { + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandFeedbackService: CommandFeedbackService + + @InjectMockKs private lateinit var rspCommandResultHandler: RspCommandResultHandler + + @Test + fun handleSuccess() { + val command = CommandFactory.rspCommandInProgress() + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendSuccessFeedback(any()) } + + rspCommandResultHandler.handleSuccess(command) + + assertThat(command.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) + verify { commandService.saveCommand(command) } + verify { commandFeedbackService.sendSuccessFeedback(command) } + } + + @Test + fun handleFailure() { + val command = CommandFactory.rspCommandInProgress() + val message = MessageFactory.messageWithUrc(listOf("PSK:HSER"), "") + every { commandService.saveCommand(any()) } answers { firstArg() } + justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } + + rspCommandResultHandler.handleFailure(command, message) + + assertThat(command.status).isEqualTo(Command.CommandStatus.ERROR) + verify { commandService.saveCommand(command) } + verify { + commandFeedbackService.sendErrorFeedback(command, match { error -> error.contains("SHA256 hash error") }) + } + } + + @ParameterizedTest + @MethodSource("hasSucceededTestSource") + fun hasSucceeded(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasSucceeded = rspCommandResultHandler.hasSucceeded(TestConstants.DEVICE_ID, message) + + assertThat(hasSucceeded).isEqualTo(expectedResult) + } + + @ParameterizedTest + @MethodSource("hasFailedTestSource") + fun hasFailed(urcs: List, downlink: String, expectedResult: Boolean) { + val message = MessageFactory.messageWithUrc(urcs, downlink) + + val hasFailed = rspCommandResultHandler.hasFailed(TestConstants.DEVICE_ID, message) + + assertThat(hasFailed).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun hasSucceededTestSource(): Stream = + Stream.of( + Arguments.of(listOf(), "CMD:RSP", true), + Arguments.of(listOf("PSK:TMP"), "!PSK:######;CMD:RSP", true), + Arguments.of(listOf("PSK:TMP"), "!CMD:RSP;PSK:######", true), + Arguments.of(listOf("RSP:DLER"), "CMD:RSP", false), + Arguments.of(listOf("RSP:DLER", "PSK:TMP"), "!CMD:RSP;PSK:######", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:SET"), "0", false), + Arguments.of(listOf("PSK:TMP", "PSK:SET"), "0", false), + ) + + @JvmStatic + fun hasFailedTestSource(): Stream = + Stream.of( + Arguments.of(listOf("RSP:DLER"), "CMD:RSP", true), + Arguments.of(listOf("RSP:DLER"), "CMD:RSP", true), + Arguments.of(listOf("RSP:DLER", "PSK:TMP"), "!CMD:RSP;PSK:######", true), + Arguments.of(listOf(), "CMD:RSP", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######;CMD:RSP", false), + Arguments.of(listOf("PSK:TMP"), "!CMD:RSP;PSK:######", false), + Arguments.of(listOf("PSK:DLER"), "!PSK:#####", false), + Arguments.of(listOf("PSK:HSER"), "0", false), + Arguments.of(listOf("INIT", "WDR"), "0", false), + Arguments.of(listOf("PSK:TMP"), "!PSK:######", false), + Arguments.of(listOf("PSK:SET"), "!PSK:#####SET", false), + ) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultServiceTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultServiceTest.kt new file mode 100644 index 00000000..364497bd --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/command/service/CommandResultServiceTest.kt @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.command.service + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import org.gxf.crestdeviceservice.CommandFactory +import org.gxf.crestdeviceservice.MessageFactory +import org.gxf.crestdeviceservice.TestConstants.DEVICE_ID +import org.gxf.crestdeviceservice.command.entity.Command +import org.gxf.crestdeviceservice.command.resulthandler.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class CommandResultServiceTest { + @MockK private lateinit var rebootCommandResultHandler: RebootCommandResultHandler + @MockK private lateinit var rspCommandResultHandler: RspCommandResultHandler + + @MockK private lateinit var commandService: CommandService + @MockK private lateinit var commandResultHandlersByType: Map + + @InjectMockKs private lateinit var commandResultService: CommandResultService + + @BeforeEach + fun setUp() { + every { commandResultHandlersByType[Command.CommandType.REBOOT] } answers { rebootCommandResultHandler } + every { commandResultHandlersByType[Command.CommandType.RSP] } answers { rspCommandResultHandler } + } + + @Test + fun shouldHandleMessageWhenCommandHasSucceeded() { + val message = MessageFactory.messageTemplate() + val command = CommandFactory.rebootCommandInProgress() + + every { commandService.getAllCommandsInProgressForDevice(any()) } returns listOf(command) + every { rebootCommandResultHandler.hasSucceeded(any(), any()) } returns true + justRun { rebootCommandResultHandler.handleSuccess(any()) } + + commandResultService.handleMessage(DEVICE_ID, message) + + verify(exactly = 1) { rebootCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 0) { rebootCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 1) { rebootCommandResultHandler.handleSuccess(command) } + verify(exactly = 0) { rebootCommandResultHandler.handleFailure(command, message) } + } + + @Test + fun shouldHandleMessageWhenCommandHasFailed() { + val message = MessageFactory.messageTemplate() + val command = CommandFactory.rspCommandInProgress() + + every { commandService.getAllCommandsInProgressForDevice(any()) } returns listOf(command) + every { rspCommandResultHandler.hasSucceeded(any(), any()) } returns false + every { rspCommandResultHandler.hasFailed(any(), any()) } returns true + justRun { rspCommandResultHandler.handleFailure(any(), any()) } + + commandResultService.handleMessage(DEVICE_ID, message) + + verify(exactly = 1) { rspCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 0) { rspCommandResultHandler.handleSuccess(command) } + verify(exactly = 1) { rspCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 1) { rspCommandResultHandler.handleFailure(command, message) } + } + + @Test + fun shouldHandleMessageWhenCommandIsStillInProgress() { + val message = MessageFactory.messageTemplate() + val command = CommandFactory.rspCommandInProgress() + + every { commandService.getAllCommandsInProgressForDevice(any()) } returns listOf(command) + every { rspCommandResultHandler.hasSucceeded(any(), any()) } returns false + every { rspCommandResultHandler.hasFailed(any(), any()) } returns false + justRun { rspCommandResultHandler.handleStillInProgress(any()) } + + commandResultService.handleMessage(DEVICE_ID, message) + + verify(exactly = 1) { rspCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 0) { rspCommandResultHandler.handleSuccess(command) } + verify(exactly = 1) { rspCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 0) { rspCommandResultHandler.handleFailure(command, message) } + verify(exactly = 1) { rspCommandResultHandler.handleStillInProgress(command) } + } + + @Test + fun shouldHandleMessageWhenMultipleCommandsHaveSucceeded() { + val message = MessageFactory.messageTemplate() + val rebootCommand = CommandFactory.rebootCommandInProgress() + val rspCommand = CommandFactory.rspCommandInProgress() + + every { commandService.getAllCommandsInProgressForDevice(any()) } returns listOf(rebootCommand, rspCommand) + every { rebootCommandResultHandler.hasSucceeded(any(), any()) } returns true + justRun { rebootCommandResultHandler.handleSuccess(any()) } + every { rspCommandResultHandler.hasSucceeded(any(), any()) } returns true + justRun { rspCommandResultHandler.handleSuccess(any()) } + + commandResultService.handleMessage(DEVICE_ID, message) + + verify(exactly = 1) { rebootCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 1) { rebootCommandResultHandler.handleSuccess(rebootCommand) } + verify(exactly = 0) { rebootCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 0) { rebootCommandResultHandler.handleFailure(rebootCommand, message) } + + verify(exactly = 1) { rspCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 1) { rspCommandResultHandler.handleSuccess(rspCommand) } + verify(exactly = 0) { rspCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 0) { rspCommandResultHandler.handleFailure(rspCommand, message) } + } + + @Test + fun handleMessageWhenOneCommandHasSucceededAndOneCommandHasFailed() { + val message = MessageFactory.messageTemplate() + val rebootCommand = CommandFactory.rebootCommandInProgress() + val rspCommand = CommandFactory.rspCommandInProgress() + + every { commandService.getAllCommandsInProgressForDevice(any()) } returns listOf(rebootCommand, rspCommand) + every { rebootCommandResultHandler.hasSucceeded(any(), any()) } returns true + justRun { rebootCommandResultHandler.handleSuccess(any()) } + every { rspCommandResultHandler.hasSucceeded(any(), any()) } returns false + every { rspCommandResultHandler.hasFailed(any(), any()) } returns true + justRun { rspCommandResultHandler.handleFailure(any(), any()) } + + commandResultService.handleMessage(DEVICE_ID, message) + + verify(exactly = 1) { rebootCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 1) { rebootCommandResultHandler.handleSuccess(rebootCommand) } + verify(exactly = 0) { rebootCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 0) { rebootCommandResultHandler.handleFailure(rebootCommand, message) } + + verify(exactly = 1) { rspCommandResultHandler.hasSucceeded(DEVICE_ID, message) } + verify(exactly = 0) { rspCommandResultHandler.handleSuccess(rspCommand) } + verify(exactly = 1) { rspCommandResultHandler.hasFailed(DEVICE_ID, message) } + verify(exactly = 1) { rspCommandResultHandler.handleFailure(rspCommand, message) } + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/PayloadServiceTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/PayloadServiceTest.kt index 0ea1ad5b..e6fe14ce 100644 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/PayloadServiceTest.kt +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/PayloadServiceTest.kt @@ -14,8 +14,9 @@ import io.mockk.verify import java.util.UUID import org.assertj.core.api.Assertions.assertThat import org.gxf.crestdeviceservice.CommandFactory -import org.gxf.crestdeviceservice.TestHelper +import org.gxf.crestdeviceservice.MessageFactory import org.gxf.crestdeviceservice.command.service.CommandFeedbackService +import org.gxf.crestdeviceservice.command.service.CommandResultService import org.gxf.crestdeviceservice.command.service.CommandService import org.gxf.crestdeviceservice.firmware.entity.Firmware import org.gxf.crestdeviceservice.firmware.service.FirmwareService @@ -26,7 +27,7 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) class PayloadServiceTest { - @MockK private lateinit var urcService: UrcService + @MockK private lateinit var commandResultService: CommandResultService @MockK private lateinit var firmwareService: FirmwareService @MockK private lateinit var commandService: CommandService @MockK(relaxed = true) private lateinit var commandFeedbackService: CommandFeedbackService @@ -37,22 +38,22 @@ class PayloadServiceTest { @Test fun shouldProcessUrcs() { - val message = TestHelper.messageTemplate() + val message = MessageFactory.messageTemplate() val downlink = Downlink() val deviceId = "device-id" - justRun { urcService.interpretUrcsInMessage(any(), any()) } + justRun { commandResultService.handleMessage(any(), any()) } payloadService.processPayload(deviceId, message, downlink) - verify { urcService.interpretUrcsInMessage(deviceId, message) } + verify { commandResultService.handleMessage(deviceId, message) } } @Test fun shouldSupplyOtaCommandForFmc() { val packetNumber = 3 val packetCount = 20 - val message = TestHelper.messageTemplate() + val message = MessageFactory.messageTemplate() message.set(DeviceMessage.FMC_FIELD, mapper.readTree(packetNumber.toString())) val downlink = Downlink() val deviceId = "device-id" @@ -61,7 +62,7 @@ class PayloadServiceTest { val firmware = Firmware(UUID.randomUUID(), firmwareName, "some-hash", null) val otaCommand = "OTA0003ABCDEFGHIJKLMNOPQRSTUVWXYZ" - justRun { urcService.interpretUrcsInMessage(any(), any()) } + justRun { commandResultService.handleMessage(any(), any()) } every { commandService.getAllCommandsInProgressForDevice(deviceId) } returns listOf(firmwareCommand) every { firmwareService.findFirmwareByName(firmwareName) } returns firmware every { firmwareService.getPacketForDevice(firmware, packetNumber, deviceId) } returns otaCommand @@ -69,7 +70,7 @@ class PayloadServiceTest { payloadService.processPayload(deviceId, message, downlink) - verify { urcService.interpretUrcsInMessage(deviceId, message) } + verify { commandResultService.handleMessage(deviceId, message) } assertThat(downlink.getDownlink()).contains(otaCommand) verify { commandFeedbackService.sendProgressFeedback(packetNumber + 1, packetCount, firmwareCommand) } diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/UrcServiceTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/UrcServiceTest.kt deleted file mode 100644 index c718286b..00000000 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/UrcServiceTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.crestdeviceservice.service - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.node.TextNode -import io.mockk.Called -import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.impl.annotations.MockK -import io.mockk.junit5.MockKExtension -import io.mockk.justRun -import io.mockk.verify -import java.util.stream.Stream -import org.assertj.core.api.Assertions.assertThat -import org.gxf.crestdeviceservice.CommandFactory -import org.gxf.crestdeviceservice.TestConstants -import org.gxf.crestdeviceservice.TestHelper -import org.gxf.crestdeviceservice.command.entity.Command -import org.gxf.crestdeviceservice.command.service.CommandFeedbackService -import org.gxf.crestdeviceservice.command.service.CommandService -import org.gxf.crestdeviceservice.psk.service.PskService -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import org.junit.jupiter.params.provider.ValueSource - -@ExtendWith(MockKExtension::class) -class UrcServiceTest { - @MockK private lateinit var pskService: PskService - @MockK private lateinit var commandService: CommandService - @MockK private lateinit var commandFeedbackService: CommandFeedbackService - - @InjectMockKs private lateinit var urcService: UrcService - - companion object { - private const val URC_FIELD = "URC" - private const val DL_FIELD = "DL" - private val PSK_DOWNLINK = - "!PSK:umU6KJ4g7Ye5ZU6o:4a3cfdd487298e2f048ebfd703a1da4800c18f2167b62192cf7dc9fd6cc4bcd3;" + - ";PSK:umU6KJ4g7Ye5ZU6o:4a3cfdd487298e2f048ebfd703a1da4800c18f2167b62192cf7dc9fd6cc4bcd3:SET" - private const val REBOOT_DOWNLINK = "!CMD:REBOOT" - private const val DEVICE_ID = TestConstants.DEVICE_ID - - @JvmStatic - private fun containingPskErrorUrcs() = - Stream.of( - listOf("PSK:DLER"), - listOf("PSK:HSER"), - listOf("TS:ERR", "PSK:DLER"), - listOf("PSK:DLER", "PSK:EQER") - ) - - @JvmStatic - private fun notContainingPskUrcs() = - Stream.of( - listOf("INIT"), - listOf("ENPD"), - listOf("TEL:RBT"), - listOf("JTR"), - listOf("WDR"), - listOf("BOR"), - listOf("EXR"), - listOf("POR"), - listOf("INIT", "BOR", "POR") - ) - } - - @Test - fun shouldChangeActiveKeyWhenSuccessUrcReceived() { - val urcs = listOf("PSK:TMP", "PSK:SET") - val pskCommandInProgress = CommandFactory.pskCommandInProgress() - val pskSetCommandInProgress = CommandFactory.pskSetCommandInProgress() - val pskCommandsInProgress = listOf(pskCommandInProgress, pskSetCommandInProgress) - val message = updateUrcInMessage(urcs, PSK_DOWNLINK) - - every { pskService.isPendingPskPresent(DEVICE_ID) } returns true - every { commandService.getAllCommandsInProgressForDevice(DEVICE_ID) } returns pskCommandsInProgress - every { commandService.saveCommand(any()) } answers { firstArg() } - justRun { pskService.changeActiveKey(any()) } - justRun { commandFeedbackService.sendSuccessFeedback(any()) } - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - - verify { pskService.changeActiveKey(DEVICE_ID) } - - assertThat(pskCommandInProgress.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) - assertThat(pskSetCommandInProgress.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) - } - - @ParameterizedTest(name = "should set pending key as invalid for {0}") - @MethodSource("containingPskErrorUrcs") - fun shouldSetPendingKeyAsInvalidWhenPskFailureUrcReceived(urcs: List) { - val pskCommandInProgress = CommandFactory.pskCommandInProgress() - val pskSetCommandInProgress = CommandFactory.pskSetCommandInProgress() - - every { pskService.isPendingPskPresent(DEVICE_ID) } returns true - every { // - commandService.getAllCommandsInProgressForDevice(DEVICE_ID) - } returns listOf(pskCommandInProgress, pskSetCommandInProgress) - every { commandService.saveCommand(any()) } answers { firstArg() } - justRun { pskService.setPendingKeyAsInvalid(any()) } - justRun { commandFeedbackService.sendErrorFeedback(any(), any()) } - - val message = updateUrcInMessage(urcs, PSK_DOWNLINK) - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - - verify { pskService.setPendingKeyAsInvalid(DEVICE_ID) } - verify(exactly = 2) { commandService.saveCommand(any()) } - verify(exactly = 2) { commandFeedbackService.sendErrorFeedback(any(), any()) } - - assertThat(pskCommandInProgress.status).isEqualTo(Command.CommandStatus.ERROR) - assertThat(pskSetCommandInProgress.status).isEqualTo(Command.CommandStatus.ERROR) - } - - @ParameterizedTest(name = "should not set pending key as invalid for {0}") - @MethodSource("notContainingPskUrcs") - fun shouldNotSetPendingKeyAsInvalidWhenOtherUrcReceived(urcs: List) { - val pskCommands = CommandFactory.pskCommandsInProgress() - - every { pskService.isPendingPskPresent(DEVICE_ID) } returns true - every { commandService.getAllCommandsInProgressForDevice(DEVICE_ID) } returns pskCommands - - val message = updateUrcInMessage(urcs, PSK_DOWNLINK) - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - - verify(exactly = 0) { pskService.setPendingKeyAsInvalid(DEVICE_ID) } - } - - @Test - fun handleSuccessUrcForRebootCommand() { - val urcs = listOf("INIT", "WDR") - val commandInProgress = CommandFactory.rebootCommandInProgress() - val message = updateUrcInMessage(urcs, REBOOT_DOWNLINK) - - every { pskService.isPendingPskPresent(DEVICE_ID) } returns false - every { commandService.getAllCommandsInProgressForDevice(DEVICE_ID) } returns listOf(commandInProgress) - every { commandService.saveCommand(any()) } answers { firstArg() } - justRun { commandFeedbackService.sendSuccessFeedback(any()) } - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - - verify { commandService.saveCommand(commandInProgress) } - verify { commandFeedbackService.sendSuccessFeedback(any()) } - - assertThat(commandInProgress.status).isEqualTo(Command.CommandStatus.SUCCESSFUL) - } - - @Test - fun shouldDoNothingIfUrcDoesNotConcernCommandInProgress() { - val urcs = listOf("ENPD") - val commandInProgress = CommandFactory.rebootCommandInProgress() - val message = updateUrcInMessage(urcs, REBOOT_DOWNLINK) - - every { pskService.isPendingPskPresent(DEVICE_ID) } returns false - every { commandService.getAllCommandsInProgressForDevice(DEVICE_ID) } returns listOf(commandInProgress) - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - - verify(exactly = 0) { commandService.saveCommands(any(), any()) } - verify { commandFeedbackService wasNot Called } - } - - @ParameterizedTest(name = "should do nothing when downlink is {0}") - @ValueSource(strings = ["0", "", " ", "\n"]) - fun shouldDoNothingWhenDownlinkIsBlank(downlink: String) { - val urcs = listOf("INIT") - val message = updateUrcInMessage(urcs, downlink) - - every { commandService.getAllCommandsInProgressForDevice(DEVICE_ID) } returns listOf() - - urcService.interpretUrcsInMessage(DEVICE_ID, message) - } - - private fun updateUrcInMessage(urcs: List, downlink: String): JsonNode { - val urcNodes = urcs.map { urc -> TextNode(urc) } - val downlinkNode = ObjectNode(JsonNodeFactory.instance, mapOf(DL_FIELD to TextNode(downlink))) - - val urcFieldValue: ArrayNode = ObjectMapper().valueToTree(urcNodes + listOf(downlinkNode)) - - val message = TestHelper.messageTemplate() - message.replace(URC_FIELD, urcFieldValue) - - return message - } -} diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt index 4a008425..d555d0ab 100644 --- a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt @@ -47,8 +47,10 @@ class FirmwareMapper(private val firmwareRepository: FirmwareRepository) { return previousVersionRegex.find(name)?.let { val previousFirmwareVersion = it.value val previousFirmware = firmwareRepository.findByVersion(previousFirmwareVersion) + if (previousFirmware == null) { + logger.warn { "Previous firmware with version $previousFirmwareVersion does not exist" } + } previousFirmware?.id - ?: throw FirmwareException("Previous firmware with version $previousFirmwareVersion does not exist") } } diff --git a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt index 6b207cc4..4f2d5565 100644 --- a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt +++ b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt @@ -3,24 +3,21 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.crestdeviceservice -import com.alliander.sng.Firmware as ExternalFirmware -import com.alliander.sng.FirmwareType -import com.alliander.sng.Firmwares import java.util.UUID -import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_FROM_VERSION -import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_NAME -import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_PACKET_0 -import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_UUID -import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_VERSION -import org.gxf.crestdeviceservice.TestConstants.PREVIOUS_FIRMWARE_UUID +import org.gxf.crestdeviceservice.FirmwareTestConstants.FIRMWARE_FROM_VERSION +import org.gxf.crestdeviceservice.FirmwareTestConstants.FIRMWARE_NAME +import org.gxf.crestdeviceservice.FirmwareTestConstants.FIRMWARE_PACKET_0 +import org.gxf.crestdeviceservice.FirmwareTestConstants.FIRMWARE_UUID +import org.gxf.crestdeviceservice.FirmwareTestConstants.FIRMWARE_VERSION +import org.gxf.crestdeviceservice.FirmwareTestConstants.PREVIOUS_FIRMWARE_UUID import org.gxf.crestdeviceservice.firmware.entity.Firmware import org.gxf.crestdeviceservice.firmware.entity.FirmwarePacket import org.springframework.core.io.ClassPathResource import org.springframework.mock.web.MockMultipartFile object FirmwareFactory { - fun getFirmwareFile(): MockMultipartFile { - val fileName = "RTU#FULL#TO#23.10.txt" + + fun getFirmwareFile(fileName: String): MockMultipartFile { val firmwareFile = ClassPathResource(fileName).file return MockMultipartFile("file", firmwareFile.name, "text/plain", firmwareFile.readBytes()) } @@ -32,28 +29,15 @@ object FirmwareFactory { return firmware } + fun getFirmwareEntityWithoutPreviousVersion(name: String): Firmware { + val firmware = Firmware(FIRMWARE_UUID, name, FIRMWARE_VERSION, null, mutableListOf()) + val packet = getFirmwarePacket(firmware) + firmware.packets.add(packet) + return firmware + } + fun getPreviousFirmwareEntity(): Firmware = Firmware(PREVIOUS_FIRMWARE_UUID, FIRMWARE_NAME, FIRMWARE_FROM_VERSION, UUID.randomUUID(), mutableListOf()) private fun getFirmwarePacket(firmware: Firmware) = FirmwarePacket(firmware, 0, FIRMWARE_PACKET_0) - - fun getFirmwares() = Firmwares.newBuilder().setFirmwares(listOf(firmware(), previousFirmware())).build() - - private fun firmware() = - ExternalFirmware.newBuilder() - .setName(FIRMWARE_NAME) - .setType(FirmwareType.device) - .setVersion(FIRMWARE_VERSION) - .setFromVersion(FIRMWARE_FROM_VERSION) - .setNumberOfPackages(2) - .build() - - private fun previousFirmware() = - ExternalFirmware.newBuilder() - .setName(FIRMWARE_NAME) - .setType(FirmwareType.device) - .setVersion(FIRMWARE_FROM_VERSION) - .setFromVersion(FIRMWARE_FROM_VERSION) - .setNumberOfPackages(2) - .build() } diff --git a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareTestConstants.kt similarity index 59% rename from components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt rename to components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareTestConstants.kt index 4c25c8a9..eb649ff1 100644 --- a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt +++ b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareTestConstants.kt @@ -5,15 +5,20 @@ package org.gxf.crestdeviceservice import java.util.UUID -object TestConstants { +object FirmwareTestConstants { + const val FIRMWARE_FILE_FULL_NAME = "RTU#FULL#TO#23.10.txt" + + const val FIRMWARE_FILE_DELTA_NAME = "RTU#DELTA#FROM#23.10#TO#24.00.txt" + const val FIRMWARE_FILE_DELTA_VERSION = "24.00" + const val FIRMWARE_FILE_DELTA_PREVIOUS_VERSION = "23.10" + const val FIRMWARE_FILE_DELTA_PACKET_SIZE = 6 + const val FIRMWARE_VERSION = "01.10" const val FIRMWARE_FROM_VERSION = "01.20" const val FIRMWARE_NAME = "RTU#DELTA#FROM#$FIRMWARE_FROM_VERSION#TO#$FIRMWARE_VERSION" const val FIRMWARE_WRONG_NAME = "FOTA.txt" const val FIRMWARE_PACKET_0 = "OTA0000or^kx?6`+01uK8B#)DylY_B=aFCG>Cq57j5CD-81;B_S8Ss(dkdq82fRo-34IlsxC!djzkq?j\$z7LQekQkNM5Dh>8g8PUkfccP-4=0%;De!?2KoAX300Y32;En+U*wkqRfs5Dl;Zfe{42h2Vgb3MVO=K#?XVQxFZn0EOb0;trHkGXIh9k-w3zk\$eykPym3D3MPRO2f\$yL5Rw%Ff#Bs35O4s3;DD0~CMuXKm?@Z9nt+jk5D<_6fRPC&fe{wK4wQEikl=uo_Yfrz07uV&;0Tp~k!L4{m0Zn;4=xZTAOL`qXD48jX(vFFXeS7j|C09*B|rcOFYu6PClHZoC*s_J5fH%i5G7CmfRSe>1KoiU3&0;K5NIIc-4G>!0AS#7;L8Cdz<`lwCy!s|+`JGaumBA!{17tG0DzGcCyFEBQfxU;MWj25CA3MHsV9=THs;ecHj<_c?x6@Ij{iUw>*g8ksBuKFbME\$z\$XEf5IN8Q>0J6vAcHZ3LW5I-+yU&74*" - const val FIRMWARE_PACKET_1 = - "OTA00015INugu;J^U@BtAd>0J6v6oM6k-4Hqe0O?%%O+bcGhGK?uhTH+{kx~#k5CE{@>!0ue5hUqc`c0&UwT5&MIxqmh0TCoj;85U\$UYSh}l!IpC5IR5rC*lS&5(g>tF%T8N;E^ULD-b\$R00_W`;si1n2XXW<5DdWJk^K-lU;rd1@Zjzb{~zU1`b~r2kl@e|I)DJ>QTk1hf`Ed6ksBtDUx1TQ5IT?m8zz9@0lffRPy{k6(b3877Ulp0{KWJP-gRC4u1R8u17N=o8=qzyj!X5Iv9p{?CKrkl+rKGiZR51tyS_;}AWd00t%%G12fq^bs(b@M!Q9F+~tQZ~%xS5%6H+aNuB*947gh{?Bg^K9B\$\$CV=3O;0}~#Xn>I%CS%\$V5I&#)fRP*~ACixgh2@*zQpb+vdk{eI01lK5WcDtAksBuX76|Y|!7UI\$pa6&?3G~~=8Nh&%CMWn31;D`&L9hVsAPMx&z=Gftz<`k?C-@e}5JE5jiCmgZAmQmP_\$}N40gw%nX%IqS0QdpvT>4Gl0UVYSmW+`Tm&Xu7@Bn&=g#iE\$5g+&ga@_lN=}cnf}jUksK\$J5Jw;Yf#86XB_)89C?\$ZCDJ6iD_Yg-g0461XlPV>ElP4vBlPo2GllBltKmaQxfRisJfRimHfRipIE5Q5^M_>SO;K%_ElnrEnksBsx7clfF5JzwTf#86XB_&{!F(ur