From 678aeba5d0819b83d672653a84aadaa6bcaca2ec Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 8 Mar 2023 08:01:21 +0700 Subject: [PATCH 1/5] add eligibility tests --- .../hmkitfleet/network/UtilityRequestsTest.kt | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt diff --git a/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt new file mode 100644 index 0000000..3002e44 --- /dev/null +++ b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt @@ -0,0 +1,194 @@ +/* + * The MIT License + * + * Copyright (c) 2014- High-Mobility GmbH (https://high-mobility.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.highmobility.hmkitfleet.network + +import com.highmobility.hmkitfleet.BaseTest +import com.highmobility.hmkitfleet.model.Brand +import com.highmobility.hmkitfleet.model.EligibilityStatus +import com.highmobility.hmkitfleet.notExpiredAuthToken +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.net.HttpURLConnection + +internal class UtilityRequestsTest : BaseTest() { + val mockWebServer = MockWebServer() + val client = OkHttpClient() + val authToken = notExpiredAuthToken() + val authTokenRequests = mockk() + + @BeforeEach + fun setUp() { + mockWebServer.start() + coEvery { authTokenRequests.getAuthToken() } returns Response(authToken) + } + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + // get eligibility + + @Test + fun getEligibilitySuccess() { + val mockResponse = MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody( + """ + { + "vin": "WBADT43452G296403", + "eligible": true, + "data_delivery": [ + "pull", + "push" + ] + } + """.trimIndent() + ) + mockWebServer.enqueue(mockResponse) + val mockUrl = mockWebServer.url("").toString() + val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) + + val response = runBlocking { + webService.getEligibility("WBADT43452G296403", Brand.MERCEDES_BENZ) + } + + coVerify { authTokenRequests.getAuthToken() } + + val recordedRequest: RecordedRequest = mockWebServer.takeRequest() + assertTrue(recordedRequest.path!!.endsWith("/eligibility")) + + // verify request + assertTrue(recordedRequest.headers["Authorization"] == "Bearer ${authToken.authToken}") + val jsonBody = Json.parseToJsonElement(recordedRequest.body.readUtf8()) + assertTrue(jsonBody.jsonObject["vin"]?.jsonPrimitive?.contentOrNull == "WBADT43452G296403") + assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "mercedes-benz") + + // verify response + val status = response.response!! + assertTrue(status.vin == "WBADT43452G296403") + assertTrue(status.eligible) + assertTrue(status.dataDelivery.size == 2) + assertTrue(status.dataDelivery.find { it == EligibilityStatus.DataDelivery.PULL } != null) + assertTrue(status.dataDelivery.find { it == EligibilityStatus.DataDelivery.PUSH } != null) + assertTrue(status.connectivityStatus == null) + assertTrue(status.primaryUserAssigned == null) + } + + @Test + fun getEligibilitySuccessOptionalValues() { + val mockResponse = MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody( + """ + { + "vin": "WBADT43452G296403", + "eligible": true, + "data_delivery": [ + "pull", + "push" + ], + "connectivity_status": "deactivated", + "primary_user_assigned": true + } + """.trimIndent() + ) + + mockWebServer.enqueue(mockResponse) + val mockUrl = mockWebServer.url("").toString() + val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) + + val status = runBlocking { + webService.getEligibility("WBADT43452G296403", Brand.MERCEDES_BENZ) + }.response!! + + coVerify { authTokenRequests.getAuthToken() } + + val recordedRequest: RecordedRequest = mockWebServer.takeRequest() + assertTrue(recordedRequest.path!!.endsWith("/eligibility")) + + // verify request + assertTrue(recordedRequest.headers["Authorization"] == "Bearer ${authToken.authToken}") + val jsonBody = Json.parseToJsonElement(recordedRequest.body.readUtf8()) + assertTrue(jsonBody.jsonObject["vin"]?.jsonPrimitive?.contentOrNull == "WBADT43452G296403") + assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "mercedes-benz") + + // verify response + assertTrue(status.vin == "WBADT43452G296403") + assertTrue(status.eligible) + assertTrue(status.dataDelivery.size == 2) + assertTrue(status.dataDelivery.find { it == EligibilityStatus.DataDelivery.PULL } != null) + assertTrue(status.dataDelivery.find { it == EligibilityStatus.DataDelivery.PUSH } != null) + + assertTrue(status.connectivityStatus == EligibilityStatus.ConnectivityStatus.DEACTIVATED) + assertTrue(status.primaryUserAssigned == true) + } + + @Test + fun requestClearanceAuthTokenError() = runBlocking { + testAuthTokenErrorReturned(mockWebServer, authTokenRequests) { mockUrl -> + val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) + webService.getEligibility( + "WBADT43452G296403", + Brand.MERCEDES_BENZ, + ) + } + } + + @Test + fun requestClearanceErrorResponse() = runBlocking { + testErrorResponseReturned(mockWebServer) { mockUrl -> + val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) + webService.getEligibility( + "WBADT43452G296403", + Brand.MERCEDES_BENZ, + ) + } + } + + @Test + fun requestClearanceUnknownResponse() = runBlocking { + testForUnknownResponseGenericErrorReturned(mockWebServer) { mockUrl -> + val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) + webService.getEligibility( + "WBADT43452G296403", + Brand.MERCEDES_BENZ, + ) + } + } +} From 7bf3d3d6c77cbfc62c7c3c3da1cd58678ca3deab Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 8 Mar 2023 08:38:07 +0700 Subject: [PATCH 2/5] implement eligibility --- hmkit-fleet/src/main/kotlin/HMKitFleet.kt | 17 +++- hmkit-fleet/src/main/kotlin/Koin.kt | 8 ++ .../main/kotlin/model/EligibilityStatus.kt | 60 ++++++++++++ .../main/kotlin/network/UtilityRequests.kt | 91 +++++++++++++++++++ 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 hmkit-fleet/src/main/kotlin/model/EligibilityStatus.kt create mode 100644 hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt diff --git a/hmkit-fleet/src/main/kotlin/HMKitFleet.kt b/hmkit-fleet/src/main/kotlin/HMKitFleet.kt index 16e2f80..0530fae 100644 --- a/hmkit-fleet/src/main/kotlin/HMKitFleet.kt +++ b/hmkit-fleet/src/main/kotlin/HMKitFleet.kt @@ -35,7 +35,7 @@ import java.util.concurrent.CompletableFuture /** * HMKitFleet is the access point for the Fleet SDK functionality. It is accessed by - * HMKitFleet.INSTANCE and it's field [configuration] should be set before accessing other functions + * HMKitFleet.INSTANCE. It's field [configuration] should be set before accessing other functions */ object HMKitFleet { init { @@ -59,6 +59,21 @@ object HMKitFleet { */ lateinit var configuration: ServiceAccountApiConfiguration + /** + * Get the eligibility status for a specific VIN. This can be used to find out if the vehicle has the necessary connectivity to transmit data. + * + * @param vin The vehicle VIN number + * @param brand The vehicle brand + * @return The eligibility status + */ + fun getEligibility( + vin: String, + brand: Brand + ): CompletableFuture> = GlobalScope.future { + logger.debug("HMKitFleet: getEligibility: $vin") + koin.get().getEligibility(vin, brand) + } + /** * Start the data access clearance process for a vehicle. * diff --git a/hmkit-fleet/src/main/kotlin/Koin.kt b/hmkit-fleet/src/main/kotlin/Koin.kt index 35133db..cc5dee3 100644 --- a/hmkit-fleet/src/main/kotlin/Koin.kt +++ b/hmkit-fleet/src/main/kotlin/Koin.kt @@ -86,6 +86,14 @@ internal object Koin { get() ) } + single { + UtilityRequests( + get(), + get(), + HMKitFleet.environment.url, + get() + ) + } } lateinit var koinApplication: KoinApplication diff --git a/hmkit-fleet/src/main/kotlin/model/EligibilityStatus.kt b/hmkit-fleet/src/main/kotlin/model/EligibilityStatus.kt new file mode 100644 index 0000000..d59a6b1 --- /dev/null +++ b/hmkit-fleet/src/main/kotlin/model/EligibilityStatus.kt @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright (c) 2014- High-Mobility GmbH (https://high-mobility.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.highmobility.hmkitfleet.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EligibilityStatus( + val vin: String, + val eligible: Boolean, + @SerialName("data_delivery") + val dataDelivery: List = emptyList(), + @SerialName("connectivity_status") + val connectivityStatus: ConnectivityStatus? = null, + @SerialName("primary_user_assigned") + val primaryUserAssigned: Boolean? = null +) { + @Serializable + enum class DataDelivery { + @SerialName("pull") + PULL, + + @SerialName("push") + PUSH + } + + @Serializable + enum class ConnectivityStatus { + @SerialName("activated") + ACTIVATED, + + @SerialName("deactivated") + DEACTIVATED, + + @SerialName("unknown") + UNKNOWN + } +} \ No newline at end of file diff --git a/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt b/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt new file mode 100644 index 0000000..0686ade --- /dev/null +++ b/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Copyright (c) 2014- High-Mobility GmbH (https://high-mobility.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.highmobility.hmkitfleet.network + +import kotlinx.serialization.json.* +import com.highmobility.hmkitfleet.model.Brand +import com.highmobility.hmkitfleet.model.ControlMeasure +import com.highmobility.hmkitfleet.model.ClearanceStatus +import com.highmobility.hmkitfleet.model.EligibilityStatus +import com.highmobility.hmkitfleet.model.RequestClearanceResponse +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.slf4j.Logger +import utils.await +import java.net.HttpURLConnection + +internal class UtilityRequests( + client: OkHttpClient, + logger: Logger, + baseUrl: String, + private val authTokenRequests: AuthTokenRequests +) : Requests( + client, + logger, baseUrl +) { + suspend fun getEligibility( + vin: String, + brand: Brand + ): Response { + val body = requestBody(vin, brand) + val authToken = authTokenRequests.getAuthToken() + + if (authToken.error != null) return Response(null, authToken.error) + + val request = Request.Builder() + .url("${baseUrl}/eligibility") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer ${authToken.response?.authToken}") + .post(body) + .build() + + printRequest(request) + + val call = client.newCall(request) + val response = call.await() + + return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody -> + val eligibilityStatus = Json.decodeFromString(responseBody) + if (eligibilityStatus.vin != vin) logger.warn("VIN in response does not match VIN in request") + Response(eligibilityStatus, null) + } + } + + private fun requestBody( + vin: String, + brand: Brand, + ): RequestBody { + val vehicle = buildJsonObject { + put("vin", vin) + put("brand", Json.encodeToJsonElement(brand)) + } + + val body = Json.encodeToString(vehicle).toRequestBody(mediaType) + return body + } +} \ No newline at end of file From 8d2d507ef16465013b2fa5853a4d866bdc0df5a4 Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 8 Mar 2023 08:49:25 +0700 Subject: [PATCH 3/5] set version to 0.7.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fa5cb00..b21c139 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.6.7 +version=0.7.0 kotlin.code.style=official From bae13fa8d76b2fc4f6434cad2c9c4af8109302ce Mon Sep 17 00:00:00 2001 From: tonis Date: Fri, 10 Mar 2023 15:43:03 +0700 Subject: [PATCH 4/5] use bmw in tests --- .../hmkitfleet/network/UtilityRequestsTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt index 3002e44..c3204d2 100644 --- a/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt +++ b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/UtilityRequestsTest.kt @@ -85,7 +85,7 @@ internal class UtilityRequestsTest : BaseTest() { val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) val response = runBlocking { - webService.getEligibility("WBADT43452G296403", Brand.MERCEDES_BENZ) + webService.getEligibility("WBADT43452G296403", Brand.BMW) } coVerify { authTokenRequests.getAuthToken() } @@ -97,7 +97,7 @@ internal class UtilityRequestsTest : BaseTest() { assertTrue(recordedRequest.headers["Authorization"] == "Bearer ${authToken.authToken}") val jsonBody = Json.parseToJsonElement(recordedRequest.body.readUtf8()) assertTrue(jsonBody.jsonObject["vin"]?.jsonPrimitive?.contentOrNull == "WBADT43452G296403") - assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "mercedes-benz") + assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "bmw") // verify response val status = response.response!! @@ -134,7 +134,7 @@ internal class UtilityRequestsTest : BaseTest() { val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) val status = runBlocking { - webService.getEligibility("WBADT43452G296403", Brand.MERCEDES_BENZ) + webService.getEligibility("WBADT43452G296403", Brand.BMW) }.response!! coVerify { authTokenRequests.getAuthToken() } @@ -146,7 +146,7 @@ internal class UtilityRequestsTest : BaseTest() { assertTrue(recordedRequest.headers["Authorization"] == "Bearer ${authToken.authToken}") val jsonBody = Json.parseToJsonElement(recordedRequest.body.readUtf8()) assertTrue(jsonBody.jsonObject["vin"]?.jsonPrimitive?.contentOrNull == "WBADT43452G296403") - assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "mercedes-benz") + assertTrue(jsonBody.jsonObject["brand"]?.jsonPrimitive?.contentOrNull == "bmw") // verify response assertTrue(status.vin == "WBADT43452G296403") @@ -165,7 +165,7 @@ internal class UtilityRequestsTest : BaseTest() { val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) webService.getEligibility( "WBADT43452G296403", - Brand.MERCEDES_BENZ, + Brand.BMW, ) } } @@ -176,7 +176,7 @@ internal class UtilityRequestsTest : BaseTest() { val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) webService.getEligibility( "WBADT43452G296403", - Brand.MERCEDES_BENZ, + Brand.BMW, ) } } @@ -187,7 +187,7 @@ internal class UtilityRequestsTest : BaseTest() { val webService = UtilityRequests(client, mockLogger, mockUrl, authTokenRequests) webService.getEligibility( "WBADT43452G296403", - Brand.MERCEDES_BENZ, + Brand.BMW, ) } } From 7335b65ee313e060309a1bcd0e3fdc7e5b516ef7 Mon Sep 17 00:00:00 2001 From: tonis Date: Fri, 10 Mar 2023 15:59:20 +0700 Subject: [PATCH 5/5] use 0.7.0 --- build.gradle | 2 +- hmkit-fleet-consumer | 2 +- settings.gradle | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d589a1f..8003671 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { coroutinesVersion = '1.6.4' koinVersion = '3.2.0' ver = [ - "hmkit-fleet" : "0.6.5", + "hmkit-fleet" : "0.7.0", "hmkit-crypto-telematics": "0.1", "hmkit-auto-api" : "13.1.1", ] diff --git a/hmkit-fleet-consumer b/hmkit-fleet-consumer index b6bfd8d..ad5f923 160000 --- a/hmkit-fleet-consumer +++ b/hmkit-fleet-consumer @@ -1 +1 @@ -Subproject commit b6bfd8dac0bcd7fc2d282a4e6ea1d5d4a9cb8c25 +Subproject commit ad5f923365b9853d766909f0022eea578acffe0a diff --git a/settings.gradle b/settings.gradle index 2e5eec8..0792c89 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,9 +3,11 @@ rootProject.name = 'hmkit-fleet' include ':hmkit-fleet', ':hmkit-crypto-telematics', ':hmkit-fleet-consumer', - ':hmkit-utils' + ':hmkit-utils', + ':vehicle-access-store' project(':hmkit-fleet').projectDir = new File('hmkit-fleet') +project(':vehicle-access-store').projectDir = new File('hmkit-fleet-consumer/vehicle-access-store') project(':hmkit-fleet-consumer').projectDir = new File('hmkit-fleet-consumer/hmkit-fleet-consumer') project(':hmkit-utils').projectDir = new File('hmkit-crypto-java/hmkit-utils-java') project(':hmkit-crypto-telematics').projectDir = new File('hmkit-crypto-java')