From b2c8def564841c51cd5e1965a9f3057714751099 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 19 Oct 2023 12:37:13 -0700 Subject: [PATCH 01/17] (liveness) retry websocket connection by parsing string message --- .../predictions/aws/http/AWSV4Signer.kt | 5 ++ .../predictions/aws/http/LivenessWebSocket.kt | 52 +++++++++++++++---- .../liveness/InvalidSignatureException.kt | 28 ++++++++++ .../models/liveness/LivenessResponseStream.kt | 3 +- 4 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt index a93452907d..2da96d3027 100755 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/AWSV4Signer.kt @@ -56,6 +56,11 @@ internal class AWSV4Signer { timeFormatter.isLenient = false } + // used in incorrect time flow where we send an invalid response first to get time offset + fun resetPriorSignature() { + priorSignature = "" + } + fun getSignedUri( uri: URI, credentials: Credentials, diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 2009ca60a3..68ec864aa0 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -46,7 +46,9 @@ import com.amplifyframework.util.UserAgent import java.net.URI import java.net.URLDecoder import java.nio.ByteBuffer +import java.text.SimpleDateFormat import java.util.Date +import java.util.Locale import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -76,6 +78,10 @@ internal class LivenessWebSocket( private val signer = AWSV4Signer() private var credentials: Credentials? = null + private val sdf = SimpleDateFormat("yyyymmddhhmmss", Locale.US) + internal var offset = 0L + internal var closeExpected = false + @VisibleForTesting internal var webSocket: WebSocket? = null internal val challengeId = UUID.randomUUID().toString() @@ -111,6 +117,18 @@ internal class LivenessWebSocket( ) } else if (livenessResponse.disconnectionEvent != null) { this@LivenessWebSocket.webSocket?.close(1000, "Liveness flow completed.") + } else if (livenessResponse.invalidSignatureException != null) { + // server will close the websocket first + closeExpected = true + // device time is set incorrectly; skew time and retry + val regex = ( + """Signature not yet current: (\d+T\d+)Z is still later than (\d+T\d+)Z""" + + """ \((\d+T\d+)Z \+ 5 min\.\)""" + ).toRegex() + val matchResult = regex.find(livenessResponse.invalidSignatureException.message)!! + val (sent, _, real) = matchResult.destructured + offset = parse(real) - parse(sent) + start() } else { handleWebSocketError(livenessResponse) } @@ -130,7 +148,9 @@ internal class LivenessWebSocket( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { LOG.debug("WebSocket onClosed") super.onClosed(webSocket, code, reason) - if (code != 1000 && !clientStoppedSession) { + if (closeExpected) { + // do nothing; we expected the server to close the connection + } else if (code != 1000 && !clientStoppedSession) { val faceLivenessException = webSocketError ?: PredictionsException( "An error occurred during the face liveness check.", reason @@ -155,6 +175,11 @@ internal class LivenessWebSocket( } } + private fun parse(timestamp: String): Long { + val cleanedTimestamp = timestamp.filterNot { it == 'T' || it == 'Z' } + return sdf.parse(cleanedTimestamp)?.time!! + } + fun start() { val userAgent = getUserAgent() @@ -173,7 +198,10 @@ internal class LivenessWebSocket( try { val credentials = credentialsProvider.resolve(emptyAttributes()) this@LivenessWebSocket.credentials = credentials - val signedUri = signer.getSignedUri(URI.create(endpoint), credentials, region, userAgent) + signer.resetPriorSignature() + val signedUri = signer.getSignedUri( + URI.create(endpoint), credentials, region, userAgent, Date().time + offset + ) if (signedUri != null) { val signedEndpoint = URLDecoder.decode(signedUri.toString(), "UTF-8") val signedEndpointNoSpaces = signedEndpoint.replace(" ", signer.encodedSpace) @@ -215,6 +243,8 @@ internal class LivenessWebSocket( } private fun startWebSocket(okHttpClient: OkHttpClient, url: String) { + closeExpected = false + okHttpClient.newWebSocket( Request.Builder().url(url).build(), webSocketListener @@ -274,14 +304,14 @@ internal class LivenessWebSocket( videoStartTime: Long ) { // Send initial ClientSessionInformationEvent - videoStartTimestamp = videoStartTime + videoStartTimestamp = videoStartTime + offset initialDetectedFace = BoundingBox( left = initialFaceRect.left / sessionInformation.videoWidth, top = initialFaceRect.top / sessionInformation.videoHeight, height = initialFaceRect.height() / sessionInformation.videoHeight, width = initialFaceRect.width() / sessionInformation.videoWidth ) - faceDetectedStart = videoStartTime + faceDetectedStart = videoStartTime + offset val clientInfoEvent = ClientSessionInformationEvent( challenge = ClientChallenge( @@ -309,8 +339,8 @@ internal class LivenessWebSocket( initialFaceDetectedTimestamp = faceDetectedStart ), targetFace = TargetFace( - faceDetectedInTargetPositionStartTimestamp = faceMatchedStart, - faceDetectedInTargetPositionEndTimestamp = faceMatchedEnd, + faceDetectedInTargetPositionStartTimestamp = faceMatchedStart + offset, + faceDetectedInTargetPositionEndTimestamp = faceMatchedEnd + offset, boundingBox = BoundingBox( left = targetFaceRect.left / sessionInformation.videoWidth, top = targetFaceRect.top / sessionInformation.videoHeight, @@ -338,7 +368,7 @@ internal class LivenessWebSocket( currentColor = currentColor, previousColor = previousColor, sequenceNumber = sequenceNumber, - currentColorStartTimestamp = colorStartTime + currentColorStartTimestamp = colorStartTime + offset ) ) ) @@ -358,7 +388,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val eventDate = Date() + val eventDate = Date(Date().time + offset) val signedPayload = signer.getSignedFrame( region, encodedPayload.array(), @@ -381,12 +411,12 @@ internal class LivenessWebSocket( fun sendVideoEvent(videoBytes: ByteArray, videoEventTime: Long) { if (videoBytes.isNotEmpty()) { - videoEndTimestamp = videoEventTime + videoEndTimestamp = videoEventTime + offset } credentials?.let { val videoBuffer = ByteBuffer.wrap(videoBytes) val videoEvent = VideoEvent( - timestampMillis = videoEventTime, + timestampMillis = videoEventTime + offset, videoChunk = videoBuffer ) val videoJsonString = Json.encodeToString(videoEvent) @@ -399,7 +429,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val videoEventDate = Date() + val videoEventDate = Date(Date().time + offset) val signedVideoPayload = signer.getSignedFrame( region, encodedVideoPayload.array(), diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt new file mode 100644 index 0000000000..9e484960da --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Constructs a new AccessDeniedException with the specified error message. + * + * @param message Describes the error encountered. + */ +@Serializable +internal data class InvalidSignatureException( + @SerialName("Message") override val message: String +) : Exception(message) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt index f6348ac9ad..0d9c6e60d8 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt @@ -29,5 +29,6 @@ internal data class LivenessResponseStream( ServiceQuotaExceededException? = null, @SerialName("ServiceUnavailableException") val serviceUnavailableException: ServiceUnavailableException? = null, @SerialName("SessionNotFoundException") val sessionNotFoundException: SessionNotFoundException? = null, - @SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null + @SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null, + @SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null ) From 04ea724c9561a6709b8ffdd48f2241da2099f15b Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Mon, 23 Oct 2023 16:24:01 -0700 Subject: [PATCH 02/17] read timestamp from `Date` header instead of gross regex of message body --- .../predictions/aws/http/LivenessWebSocket.kt | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 68ec864aa0..282c26b860 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -92,13 +92,30 @@ internal class LivenessWebSocket( private var webSocketError: PredictionsException? = null internal var clientStoppedSession = false val json = Json { ignoreUnknownKeys = true } + val FIVE_MINUTES = 1000 * 60 * 5 @VisibleForTesting internal var webSocketListener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { LOG.debug("WebSocket onOpen") - super.onOpen(webSocket, response) - this@LivenessWebSocket.webSocket = webSocket + + // device time may be set incorrectly; read the header to skew time and retry + val sdf = SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US) + val date = response.header("Date")?.let { sdf.parse(it) } + val tempOffset = if (date != null) { + date.time - (Date().time + offset) + } else 0 + + // if offset is > 5 minutes, server will reject the request + if (kotlin.math.abs(tempOffset) < FIVE_MINUTES) { + super.onOpen(webSocket, response) + this@LivenessWebSocket.webSocket = webSocket + } else { + // server will close this websocket, don't report that failure back + closeExpected = true + offset = tempOffset + start() + } } override fun onMessage(webSocket: WebSocket, text: String) { @@ -117,18 +134,6 @@ internal class LivenessWebSocket( ) } else if (livenessResponse.disconnectionEvent != null) { this@LivenessWebSocket.webSocket?.close(1000, "Liveness flow completed.") - } else if (livenessResponse.invalidSignatureException != null) { - // server will close the websocket first - closeExpected = true - // device time is set incorrectly; skew time and retry - val regex = ( - """Signature not yet current: (\d+T\d+)Z is still later than (\d+T\d+)Z""" + - """ \((\d+T\d+)Z \+ 5 min\.\)""" - ).toRegex() - val matchResult = regex.find(livenessResponse.invalidSignatureException.message)!! - val (sent, _, real) = matchResult.destructured - offset = parse(real) - parse(sent) - start() } else { handleWebSocketError(livenessResponse) } From 42a089441ea8f362e6cc96a9e392f36a15c82466 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 26 Oct 2023 18:38:33 -0700 Subject: [PATCH 03/17] add test to web socket autoskew --- .../predictions/aws/http/LivenessWebSocket.kt | 8 +-- .../aws/http/LivenessWebSocketTest.kt | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 282c26b860..5c08d84366 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -93,6 +93,7 @@ internal class LivenessWebSocket( internal var clientStoppedSession = false val json = Json { ignoreUnknownKeys = true } val FIVE_MINUTES = 1000 * 60 * 5 + val datePattern = "EEE, d MMM yyyy HH:mm:ss z" @VisibleForTesting internal var webSocketListener = object : WebSocketListener() { @@ -100,7 +101,7 @@ internal class LivenessWebSocket( LOG.debug("WebSocket onOpen") // device time may be set incorrectly; read the header to skew time and retry - val sdf = SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US) + val sdf = SimpleDateFormat(datePattern, Locale.US) val date = response.header("Date")?.let { sdf.parse(it) } val tempOffset = if (date != null) { date.time - (Date().time + offset) @@ -180,11 +181,6 @@ internal class LivenessWebSocket( } } - private fun parse(timestamp: String): Long { - val cleanedTimestamp = timestamp.filterNot { it == 'T' || it == 'Z' } - return sdf.parse(cleanedTimestamp)?.time!! - } - fun start() { val userAgent = getUserAgent() diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 7ab4b99cf9..1769f522fe 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -35,11 +35,16 @@ import com.amplifyframework.predictions.aws.models.liveness.ServerSessionInforma import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.aws.models.liveness.ValidationException import com.amplifyframework.predictions.models.FaceLivenessSessionInformation +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.verify import java.net.URL +import java.text.SimpleDateFormat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.Date +import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.resetMain @@ -48,6 +53,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import mockwebserver3.MockResponse import mockwebserver3.MockWebServer +import okhttp3.Headers import okhttp3.Protocol import okhttp3.Request import okhttp3.Response @@ -64,6 +70,8 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.util.TimeZone +import kotlin.math.abs @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) @@ -317,6 +325,57 @@ internal class LivenessWebSocketTest { assertEquals(livenessWebSocket.getUserAgent(), "$baseline $additional") } + @Test + fun `web socket detects clock skew from server response`() { + val livenessWebSocket = createLivenessWebSocket() + mockkConstructor(WebSocket::class) + val socket: WebSocket = mockk() + livenessWebSocket.webSocket = socket + val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.US) + + // server responds saying time is actually 1 hour in the future + val oneHour = 1000 * 3600 + val futureDate = sdf.format(Date(Date().time + oneHour)) + val response: Response = mockk() + every { response.headers }.returns(Headers.headersOf("Date", futureDate)) + every { response.header("Date") }.returns(futureDate) + livenessWebSocket.webSocketListener.onOpen(socket, response) + + // now we should restart the websocket with an adjusted time + val openLatch = CountDownLatch(1) + val latchingListener = LatchingWebSocketResponseListener( + livenessWebSocket.webSocketListener, + openLatch = openLatch + ) + livenessWebSocket.webSocketListener = latchingListener + + server.enqueue(MockResponse().withWebSocketUpgrade(ServerWebSocketListener())) + server.start() + livenessWebSocket.start() + + openLatch.await(3, TimeUnit.SECONDS) + + assertTrue(livenessWebSocket.webSocket != null) + val originalRequest = livenessWebSocket.webSocket!!.request() + + // make sure that followup request sends offset date + val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US) + sdfGMT.timeZone = TimeZone.getTimeZone("GMT") + val sentDate = originalRequest.url.queryParameter("X-Amz-Date") ?.let { sdfGMT.parse(it) } + val diff = abs(Date().time - sentDate?.time!!) + assert(oneHour - 10000 < diff && diff < oneHour + 10000) + + // also make sure that followup request is valid + assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) + assertTrue( + originalRequest.url.queryParameter("X-Amz-Credential")!!.endsWith("//rekognition/aws4_request") + ) + assertEquals("299", originalRequest.url.queryParameter("X-Amz-Expires")) + assertEquals("host", originalRequest.url.queryParameter("X-Amz-SignedHeaders")) + assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) + assertNotNull("x-amz-user-agent") + } + @Test @Ignore("Need to work on parsing the onMessage byteString from ServerWebSocketListener") fun `sendInitialFaceDetectedEvent test`() { From 94a5757ab147760dc91219d30256d085f266689a Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Fri, 27 Oct 2023 19:42:18 -0700 Subject: [PATCH 04/17] bump Kotlin SDK + Smithy version --- .../predictions/aws/http/LivenessWebSocketTest.kt | 8 ++++---- gradle/libs.versions.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 1769f522fe..e1920620ec 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -41,10 +41,12 @@ import io.mockk.mockkConstructor import io.mockk.verify import java.net.URL import java.text.SimpleDateFormat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit import java.util.Date import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.math.abs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.resetMain @@ -70,8 +72,6 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import java.util.TimeZone -import kotlin.math.abs @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2059cd784a..c8ff34905c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,9 +15,9 @@ androidx-test-junit = "1.1.2" androidx-test-orchestrator = "1.3.0" androidx-test-runner = "1.3.0" androidx-workmanager = "2.7.1" -aws-kotlin = "0.29.1-beta" # ensure proper aws-smithy version also set +aws-kotlin = "0.33.0-beta" # ensure proper aws-smithy version also set aws-sdk = "2.62.2" -aws-smithy = "0.25.0" # ensure proper aws-kotlin version also set +aws-smithy = "0.28.0" # ensure proper aws-kotlin version also set coroutines = "1.6.3" desugar = "1.2.0" espresso = "3.3.0" From f4349782d24704729dfe9f27da7573b737d3b030 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Mon, 30 Oct 2023 14:05:29 -0700 Subject: [PATCH 05/17] fix copy/paste mistake --- .../aws/models/liveness/InvalidSignatureException.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt index 9e484960da..42cfa685ef 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/InvalidSignatureException.kt @@ -18,7 +18,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * Constructs a new AccessDeniedException with the specified error message. + * Constructs a new InvalidSignatureException with the specified error message. * * @param message Describes the error encountered. */ From d0f202e6ea77148254f810e74579a7e16a3db7ab Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Tue, 31 Oct 2023 11:19:26 -0700 Subject: [PATCH 06/17] upgrade Kotlin version to match Kotlin SDK compilation (build fails currently) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8ff34905c..9f66a4ca08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ json = "20210307" jsonassert = "1.5.0" junit = "4.13.2" kotest = "5.6.2" -kotlin = "1.7.10" +kotlin = "1.9.10" kotlin-serialization = "1.3.3" maplibre = "9.6.0" maplibre-annotations = "1.0.0" From c24fe75c3312bd95cfacb74776cb409999ee98fb Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 1 Nov 2023 23:18:31 -0700 Subject: [PATCH 07/17] bump versions and address Kotlin SDK changes --- aws-analytics-pinpoint/build.gradle.kts | 2 +- .../auth/cognito/AWSCognitoAuthService.kt | 4 ++-- .../amplifyframework/pinpoint/core/EventRecorder.kt | 11 +++++------ .../predictions/aws/http/LivenessWebSocket.kt | 1 - build.gradle.kts | 2 +- canaries/example/build.gradle | 2 +- gradle/libs.versions.toml | 2 +- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/aws-analytics-pinpoint/build.gradle.kts b/aws-analytics-pinpoint/build.gradle.kts index b563cc6805..c3c99f6fc4 100644 --- a/aws-analytics-pinpoint/build.gradle.kts +++ b/aws-analytics-pinpoint/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt index 4d00bf04bd..d845168276 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthService.kt @@ -18,9 +18,9 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.runtime.http.operation.customUserAgentMetadata import aws.sdk.kotlin.services.cognitoidentity.CognitoIdentityClient import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.endpoints.CognitoIdentityProviderEndpointProvider import aws.smithy.kotlin.runtime.client.RequestInterceptorContext import aws.smithy.kotlin.runtime.client.endpoints.Endpoint -import aws.smithy.kotlin.runtime.client.endpoints.EndpointProvider import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor import com.amplifyframework.statemachine.codegen.data.AuthConfiguration @@ -36,7 +36,7 @@ interface AWSCognitoAuthService { CognitoIdentityProviderClient { this.region = it.region this.endpointProvider = it.endpoint?.let { endpoint -> - EndpointProvider { Endpoint(endpoint) } + CognitoIdentityProviderEndpointProvider { Endpoint(endpoint) } } this.interceptors += object : HttpInterceptor { override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext): Any { diff --git a/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt b/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt index 519d223e36..b289c8ba4c 100644 --- a/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt +++ b/aws-pinpoint-core/src/main/java/com/amplifyframework/pinpoint/core/EventRecorder.kt @@ -40,6 +40,7 @@ import com.amplifyframework.pinpoint.core.database.PinpointDatabase import com.amplifyframework.pinpoint.core.endpointProfile.EndpointProfile import com.amplifyframework.pinpoint.core.models.PinpointEvent import com.amplifyframework.pinpoint.core.util.millisToIsoDate +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -59,7 +60,7 @@ class EventRecorder( AWS_PINPOINT_ANALYTICS_LOG_NAMESPACE.format(EventRecorder::class.java.simpleName) ) ) { - private var isSyncInProgress = false + private var isSyncInProgress = AtomicBoolean(false) private val defaultMaxSubmissionAllowed = 3 private val defaultMaxSubmissionSize = 1024 * 100 private val serviceDefinedMaxEventsPerBatch: Int = 100 @@ -79,12 +80,10 @@ class EventRecorder( } } - @Synchronized internal suspend fun submitEvents(): List { return withContext(coroutineDispatcher) { val result = runCatching { - if (!isSyncInProgress) { - isSyncInProgress = true + if (isSyncInProgress.compareAndSet(false, true)) { processEvents() } else { logger.info("Sync is already in progress, skipping") @@ -93,11 +92,11 @@ class EventRecorder( } when { result.isSuccess -> { - isSyncInProgress = false + isSyncInProgress.set(false) result.getOrNull() ?: emptyList() } else -> { - isSyncInProgress = false + isSyncInProgress.set(false) logger.error("Failed to submit events ${result.exceptionOrNull()}") emptyList() } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 5c08d84366..3bb1959868 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -78,7 +78,6 @@ internal class LivenessWebSocket( private val signer = AWSV4Signer() private var credentials: Credentials? = null - private val sdf = SimpleDateFormat("yyyymmddhhmmss", Locale.US) internal var offset = 0L internal var closeExpected = false diff --git a/build.gradle.kts b/build.gradle.kts index 0155bdb33b..a02cc75d3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:7.3.1") - classpath(kotlin("gradle-plugin", version = "1.7.10")) + classpath(kotlin("gradle-plugin", version = "1.9.10")) classpath("com.google.gms:google-services:4.3.15") classpath("org.jlleitschuh.gradle:ktlint-gradle:11.0.0") classpath("org.gradle:test-retry-gradle-plugin:1.4.1") diff --git a/canaries/example/build.gradle b/canaries/example/build.gradle index 088af1ae8b..feb90d5b24 100644 --- a/canaries/example/build.gradle +++ b/canaries/example/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.8.0" + ext.kotlin_version = "1.9.10" repositories { google() jcenter() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f66a4ca08..f9da9fe773 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ jsonassert = "1.5.0" junit = "4.13.2" kotest = "5.6.2" kotlin = "1.9.10" -kotlin-serialization = "1.3.3" +kotlin-serialization = "1.6.0" maplibre = "9.6.0" maplibre-annotations = "1.0.0" material = "1.8.0" From 632dbfd3861d6e97b105665abf9af15adf5fffe9 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 2 Nov 2023 01:34:35 -0700 Subject: [PATCH 08/17] address PR feedback --- aws-predictions/build.gradle.kts | 2 +- .../predictions/aws/http/LivenessWebSocket.kt | 51 ++++++++++++------- .../aws/http/LivenessWebSocketTest.kt | 4 +- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/aws-predictions/build.gradle.kts b/aws-predictions/build.gradle.kts index 7b64243839..9db0c3a5e9 100644 --- a/aws-predictions/build.gradle.kts +++ b/aws-predictions/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 3bb1959868..61c5100998 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -79,7 +79,22 @@ internal class LivenessWebSocket( private var credentials: Credentials? = null internal var offset = 0L - internal var closeExpected = false + internal enum class ReconnectState { + INITIAL, + RECONNECTING, + RECONNECTING_AGAIN; + + companion object { + fun next(state: ReconnectState): ReconnectState { + return when(state) { + INITIAL -> RECONNECTING + RECONNECTING -> RECONNECTING_AGAIN + RECONNECTING_AGAIN -> RECONNECTING_AGAIN + } + } + } + } + internal var reconnectState = ReconnectState.INITIAL @VisibleForTesting internal var webSocket: WebSocket? = null @@ -100,10 +115,10 @@ internal class LivenessWebSocket( LOG.debug("WebSocket onOpen") // device time may be set incorrectly; read the header to skew time and retry - val sdf = SimpleDateFormat(datePattern, Locale.US) + val sdf = SimpleDateFormat(datePattern, Locale.getDefault()) val date = response.header("Date")?.let { sdf.parse(it) } val tempOffset = if (date != null) { - date.time - (Date().time + offset) + date.time - adjustedDate() } else 0 // if offset is > 5 minutes, server will reject the request @@ -112,7 +127,7 @@ internal class LivenessWebSocket( this@LivenessWebSocket.webSocket = webSocket } else { // server will close this websocket, don't report that failure back - closeExpected = true + reconnectState = ReconnectState.next(reconnectState) offset = tempOffset start() } @@ -153,7 +168,7 @@ internal class LivenessWebSocket( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { LOG.debug("WebSocket onClosed") super.onClosed(webSocket, code, reason) - if (closeExpected) { + if (reconnectState == ReconnectState.RECONNECTING) { // do nothing; we expected the server to close the connection } else if (code != 1000 && !clientStoppedSession) { val faceLivenessException = webSocketError ?: PredictionsException( @@ -200,7 +215,7 @@ internal class LivenessWebSocket( this@LivenessWebSocket.credentials = credentials signer.resetPriorSignature() val signedUri = signer.getSignedUri( - URI.create(endpoint), credentials, region, userAgent, Date().time + offset + URI.create(endpoint), credentials, region, userAgent, adjustedDate() ) if (signedUri != null) { val signedEndpoint = URLDecoder.decode(signedUri.toString(), "UTF-8") @@ -243,8 +258,6 @@ internal class LivenessWebSocket( } private fun startWebSocket(okHttpClient: OkHttpClient, url: String) { - closeExpected = false - okHttpClient.newWebSocket( Request.Builder().url(url).build(), webSocketListener @@ -304,14 +317,14 @@ internal class LivenessWebSocket( videoStartTime: Long ) { // Send initial ClientSessionInformationEvent - videoStartTimestamp = videoStartTime + offset + videoStartTimestamp = adjustedDate(videoStartTime) initialDetectedFace = BoundingBox( left = initialFaceRect.left / sessionInformation.videoWidth, top = initialFaceRect.top / sessionInformation.videoHeight, height = initialFaceRect.height() / sessionInformation.videoHeight, width = initialFaceRect.width() / sessionInformation.videoWidth ) - faceDetectedStart = videoStartTime + offset + faceDetectedStart = adjustedDate(videoStartTime) val clientInfoEvent = ClientSessionInformationEvent( challenge = ClientChallenge( @@ -339,8 +352,8 @@ internal class LivenessWebSocket( initialFaceDetectedTimestamp = faceDetectedStart ), targetFace = TargetFace( - faceDetectedInTargetPositionStartTimestamp = faceMatchedStart + offset, - faceDetectedInTargetPositionEndTimestamp = faceMatchedEnd + offset, + faceDetectedInTargetPositionStartTimestamp = adjustedDate(faceMatchedStart), + faceDetectedInTargetPositionEndTimestamp = adjustedDate(faceMatchedEnd), boundingBox = BoundingBox( left = targetFaceRect.left / sessionInformation.videoWidth, top = targetFaceRect.top / sessionInformation.videoHeight, @@ -368,7 +381,7 @@ internal class LivenessWebSocket( currentColor = currentColor, previousColor = previousColor, sequenceNumber = sequenceNumber, - currentColorStartTimestamp = colorStartTime + offset + currentColorStartTimestamp = adjustedDate(colorStartTime) ) ) ) @@ -388,7 +401,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val eventDate = Date(Date().time + offset) + val eventDate = Date(adjustedDate()) val signedPayload = signer.getSignedFrame( region, encodedPayload.array(), @@ -411,12 +424,12 @@ internal class LivenessWebSocket( fun sendVideoEvent(videoBytes: ByteArray, videoEventTime: Long) { if (videoBytes.isNotEmpty()) { - videoEndTimestamp = videoEventTime + offset + videoEndTimestamp = adjustedDate(videoEventTime) } credentials?.let { val videoBuffer = ByteBuffer.wrap(videoBytes) val videoEvent = VideoEvent( - timestampMillis = videoEventTime + offset, + timestampMillis = adjustedDate(videoEventTime), videoChunk = videoBuffer ) val videoJsonString = Json.encodeToString(videoEvent) @@ -429,7 +442,7 @@ internal class LivenessWebSocket( ":content-type" to "application/json" ) ) - val videoEventDate = Date(Date().time + offset) + val videoEventDate = Date(adjustedDate()) val signedVideoPayload = signer.getSignedFrame( region, encodedVideoPayload.array(), @@ -454,6 +467,10 @@ internal class LivenessWebSocket( webSocket?.close(1000, null) } + fun adjustedDate(date: Long = Date().time): Long { + return date + offset + } + companion object { private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") } diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index e1920620ec..1c36a286c1 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -331,7 +331,7 @@ internal class LivenessWebSocketTest { mockkConstructor(WebSocket::class) val socket: WebSocket = mockk() livenessWebSocket.webSocket = socket - val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.US) + val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.getDefault()) // server responds saying time is actually 1 hour in the future val oneHour = 1000 * 3600 @@ -359,7 +359,7 @@ internal class LivenessWebSocketTest { val originalRequest = livenessWebSocket.webSocket!!.request() // make sure that followup request sends offset date - val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US) + val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.getDefault()) sdfGMT.timeZone = TimeZone.getTimeZone("GMT") val sentDate = originalRequest.url.queryParameter("X-Amz-Date") ?.let { sdfGMT.parse(it) } val diff = abs(Date().time - sentDate?.time!!) From 5e31713dd5896e4ecceb6b9382b10dbcd0b2b7d9 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 2 Nov 2023 13:27:28 -0700 Subject: [PATCH 09/17] bump Kotlin SDK version further for storage accel; bump serialization plugin version; reimplement bytearraycontent --- aws-auth-cognito/build.gradle.kts | 2 +- aws-auth-plugins-core/build.gradle.kts | 2 +- aws-logging-cloudwatch/build.gradle.kts | 2 +- aws-pinpoint-core/build.gradle.kts | 2 +- .../predictions/aws/http/LivenessWebSocket.kt | 2 +- gradle/libs.versions.toml | 6 +++--- .../geo/maplibre/http/AWSRequestSignerInterceptor.kt | 10 ++++++++-- testutils/build.gradle.kts | 2 +- 8 files changed, 17 insertions(+), 11 deletions(-) diff --git a/aws-auth-cognito/build.gradle.kts b/aws-auth-cognito/build.gradle.kts index 7a002160da..0a0a7e1496 100644 --- a/aws-auth-cognito/build.gradle.kts +++ b/aws-auth-cognito/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-auth-plugins-core/build.gradle.kts b/aws-auth-plugins-core/build.gradle.kts index 94f9546f5c..5c5a568869 100644 --- a/aws-auth-plugins-core/build.gradle.kts +++ b/aws-auth-plugins-core/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-logging-cloudwatch/build.gradle.kts b/aws-logging-cloudwatch/build.gradle.kts index d5b57024f4..d2295f9169 100644 --- a/aws-logging-cloudwatch/build.gradle.kts +++ b/aws-logging-cloudwatch/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-pinpoint-core/build.gradle.kts b/aws-pinpoint-core/build.gradle.kts index a4030852f1..a35762d90e 100644 --- a/aws-pinpoint-core/build.gradle.kts +++ b/aws-pinpoint-core/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 61c5100998..400d250c65 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -86,7 +86,7 @@ internal class LivenessWebSocket( companion object { fun next(state: ReconnectState): ReconnectState { - return when(state) { + return when (state) { INITIAL -> RECONNECTING RECONNECTING -> RECONNECTING_AGAIN RECONNECTING_AGAIN -> RECONNECTING_AGAIN diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9da9fe773..46a1474009 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,10 +15,10 @@ androidx-test-junit = "1.1.2" androidx-test-orchestrator = "1.3.0" androidx-test-runner = "1.3.0" androidx-workmanager = "2.7.1" -aws-kotlin = "0.33.0-beta" # ensure proper aws-smithy version also set +aws-kotlin = "0.33.1-beta" # ensure proper aws-smithy version also set aws-sdk = "2.62.2" -aws-smithy = "0.28.0" # ensure proper aws-kotlin version also set -coroutines = "1.6.3" +aws-smithy = "0.28.1" # ensure proper aws-kotlin version also set +coroutines = "1.7.3" desugar = "1.2.0" espresso = "3.3.0" fcm = "23.1.0" diff --git a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt index 1a73511c71..d92eac9b52 100644 --- a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt +++ b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt @@ -15,12 +15,11 @@ package com.amplifyframework.geo.maplibre.http -import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner +import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.Headers as AwsHeaders import aws.smithy.kotlin.runtime.http.HttpMethod -import aws.smithy.kotlin.runtime.http.content.ByteArrayContent import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.QueryParameters @@ -28,6 +27,7 @@ import aws.smithy.kotlin.runtime.net.Scheme import aws.smithy.kotlin.runtime.net.Url import aws.smithy.kotlin.runtime.net.toUrlString import aws.smithy.kotlin.runtime.util.emptyAttributes +import aws.smithy.kotlin.runtime.InternalApi import com.amplifyframework.geo.location.AWSLocationGeoPlugin import java.io.ByteArrayOutputStream import java.io.IOException @@ -41,6 +41,12 @@ import okio.Buffer internal const val AMAZON_HOST = "amazonaws.com" +// Kotlin SDK made ByteArrayContent internal +internal class ByteArrayContent(private val bytes: ByteArray) : HttpBody.Bytes() { + override val contentLength: Long = bytes.size.toLong() + override fun bytes(): ByteArray = bytes +} + /** * Interceptor that can authorize requests using AWS Signature V4 signer. */ diff --git a/testutils/build.gradle.kts b/testutils/build.gradle.kts index 63a600e402..0bfecd69d3 100644 --- a/testutils/build.gradle.kts +++ b/testutils/build.gradle.kts @@ -14,7 +14,7 @@ */ plugins { - id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" id("com.android.library") id("kotlin-android") } From b2b0ad5f7ab94ae6a3ef8ef1fe0967f157412e23 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 2 Nov 2023 15:43:06 -0700 Subject: [PATCH 10/17] fix auth tests; use correct bytearray replacement --- .../cognito/RealAWSCognitoAuthPluginTest.kt | 33 +++++++++++++++++++ .../http/AWSRequestSignerInterceptor.kt | 12 ++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt index 3fb992ce1f..032082aab6 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt @@ -512,6 +512,7 @@ class RealAWSCognitoAuthPluginTest { authService.cognitoIdentityProviderClient?.getUser(any()) } returns GetUserResponse.invoke { this.userAttributes = userAttributes + username = "" } every { @@ -1806,6 +1807,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } plugin.fetchMFAPreference(onSuccess, onError) @@ -1842,6 +1845,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, onError) @@ -1887,6 +1892,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -1937,6 +1944,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2040,6 +2049,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2090,6 +2101,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2140,6 +2153,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2190,6 +2205,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2240,6 +2257,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2290,6 +2309,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2340,6 +2361,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = null preferredMfaSetting = null + userAttributes = listOf() + username = "" } } @@ -2390,6 +2413,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -2440,6 +2465,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2490,6 +2517,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } @@ -2540,6 +2569,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SOFTWARE_TOKEN_MFA") preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + userAttributes = listOf() + username = "" } } @@ -2590,6 +2621,8 @@ class RealAWSCognitoAuthPluginTest { GetUserResponse.invoke { userMfaSettingList = listOf("SMS_MFA") preferredMfaSetting = "SMS_MFA" + userAttributes = listOf() + username = "" } } diff --git a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt index d92eac9b52..6877361f87 100644 --- a/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt +++ b/maplibre-adapter/src/main/java/com/amplifyframework/geo/maplibre/http/AWSRequestSignerInterceptor.kt @@ -15,10 +15,11 @@ package com.amplifyframework.geo.maplibre.http +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner -import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.Headers as AwsHeaders +import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.Host @@ -27,7 +28,6 @@ import aws.smithy.kotlin.runtime.net.Scheme import aws.smithy.kotlin.runtime.net.Url import aws.smithy.kotlin.runtime.net.toUrlString import aws.smithy.kotlin.runtime.util.emptyAttributes -import aws.smithy.kotlin.runtime.InternalApi import com.amplifyframework.geo.location.AWSLocationGeoPlugin import java.io.ByteArrayOutputStream import java.io.IOException @@ -41,12 +41,6 @@ import okio.Buffer internal const val AMAZON_HOST = "amazonaws.com" -// Kotlin SDK made ByteArrayContent internal -internal class ByteArrayContent(private val bytes: ByteArray) : HttpBody.Bytes() { - override val contentLength: Long = bytes.size.toLong() - override fun bytes(): ByteArray = bytes -} - /** * Interceptor that can authorize requests using AWS Signature V4 signer. */ @@ -131,7 +125,7 @@ internal class AWSRequestSignerInterceptor( ) val bodyBytes: ByteArray = getBytes(request.body) - val body2 = ByteArrayContent(bodyBytes) + val body2 = HttpBody.fromBytes(bodyBytes) val method = HttpMethod.parse(request.method) val awsRequest = HttpRequest(method, httpUrl, headers, body2) From 0e23c079d37e53a6a266528174b47e1a47f236fa Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 2 Nov 2023 17:29:49 -0700 Subject: [PATCH 11/17] fix other cognito unit tests --- .../src/test/java/featureTest/utilities/CognitoMockFactory.kt | 2 ++ ...ign_up_finishes_if_user_is_confirmed_in_the_first_step.json | 3 ++- ...nup_invokes_proper_cognito_request_and_returns_success.json | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt index 9a6dfeabae..954e800442 100644 --- a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt +++ b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt @@ -77,6 +77,7 @@ class CognitoMockFactory( this.userConfirmed = if (responseObject.containsKey("userConfirmed")) { (responseObject["userConfirmed"] as? JsonPrimitive)?.boolean ?: false } else false + this.userSub = "" } } } @@ -139,6 +140,7 @@ class CognitoMockFactory( value = "000-000-0000" } ) + username = "" } } } diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json index 156fa4a187..902a401f73 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json @@ -57,7 +57,8 @@ "signUpStep": "DONE", "additionalInfo": { } - } + }, + "userId": "" } } ] diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json index 767b69b838..2ee6d8130b 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json @@ -60,7 +60,8 @@ "destination": "user@domain.com", "deliveryMedium": "EMAIL", "attributeName": "attributeName" - } + }, + "userId": "" } } } From 328b0d39ebb105339d56bdc73d1f3d347ac15817 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 2 Nov 2023 17:38:11 -0700 Subject: [PATCH 12/17] move user id out of next step --- ..._invokes_proper_cognito_request_and_returns_success.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json index 2ee6d8130b..8ea70d469e 100644 --- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json +++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Test_that_signup_invokes_proper_cognito_request_and_returns_success.json @@ -60,9 +60,9 @@ "destination": "user@domain.com", "deliveryMedium": "EMAIL", "attributeName": "attributeName" - }, - "userId": "" - } + } + }, + "userId": "" } } ] From b8982d4d194a45197c3efe2bc1ccac8d18244aab Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Fri, 3 Nov 2023 14:54:33 -0700 Subject: [PATCH 13/17] update unit test -- no need to call start() again --- .../predictions/aws/http/LivenessWebSocketTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 1c36a286c1..ad92060a03 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -351,7 +351,6 @@ internal class LivenessWebSocketTest { server.enqueue(MockResponse().withWebSocketUpgrade(ServerWebSocketListener())) server.start() - livenessWebSocket.start() openLatch.await(3, TimeUnit.SECONDS) @@ -366,7 +365,6 @@ internal class LivenessWebSocketTest { assert(oneHour - 10000 < diff && diff < oneHour + 10000) // also make sure that followup request is valid - assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) assertTrue( originalRequest.url.queryParameter("X-Amz-Credential")!!.endsWith("//rekognition/aws4_request") ) From 55ddce808dd6d0f8f7fa90d6529ccac9376e498d Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 8 Nov 2023 09:00:29 -0800 Subject: [PATCH 14/17] use US locale; fix bug with state machine --- .../predictions/aws/http/LivenessWebSocket.kt | 4 ++-- .../predictions/aws/http/LivenessWebSocketTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 400d250c65..7804efbd86 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -115,19 +115,19 @@ internal class LivenessWebSocket( LOG.debug("WebSocket onOpen") // device time may be set incorrectly; read the header to skew time and retry - val sdf = SimpleDateFormat(datePattern, Locale.getDefault()) + val sdf = SimpleDateFormat(datePattern, Locale.US) val date = response.header("Date")?.let { sdf.parse(it) } val tempOffset = if (date != null) { date.time - adjustedDate() } else 0 + reconnectState = ReconnectState.next(reconnectState) // if offset is > 5 minutes, server will reject the request if (kotlin.math.abs(tempOffset) < FIVE_MINUTES) { super.onOpen(webSocket, response) this@LivenessWebSocket.webSocket = webSocket } else { // server will close this websocket, don't report that failure back - reconnectState = ReconnectState.next(reconnectState) offset = tempOffset start() } diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index ad92060a03..220ae3294d 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -331,7 +331,7 @@ internal class LivenessWebSocketTest { mockkConstructor(WebSocket::class) val socket: WebSocket = mockk() livenessWebSocket.webSocket = socket - val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.getDefault()) + val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.US) // server responds saying time is actually 1 hour in the future val oneHour = 1000 * 3600 @@ -358,7 +358,7 @@ internal class LivenessWebSocketTest { val originalRequest = livenessWebSocket.webSocket!!.request() // make sure that followup request sends offset date - val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.getDefault()) + val sdfGMT = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US) sdfGMT.timeZone = TimeZone.getTimeZone("GMT") val sentDate = originalRequest.url.queryParameter("X-Amz-Date") ?.let { sdfGMT.parse(it) } val diff = abs(Date().time - sentDate?.time!!) From 56c2cac9f8b02cddbf403bac0d6fc3b57e200568 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 8 Nov 2023 11:09:38 -0800 Subject: [PATCH 15/17] PR feedback -- move constants, make sure that skew occurs at most twice --- .../predictions/aws/http/LivenessWebSocket.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 7804efbd86..637764575e 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -106,8 +106,6 @@ internal class LivenessWebSocket( private var webSocketError: PredictionsException? = null internal var clientStoppedSession = false val json = Json { ignoreUnknownKeys = true } - val FIVE_MINUTES = 1000 * 60 * 5 - val datePattern = "EEE, d MMM yyyy HH:mm:ss z" @VisibleForTesting internal var webSocketListener = object : WebSocketListener() { @@ -148,7 +146,7 @@ internal class LivenessWebSocket( livenessResponse.serverSessionInformationEvent.sessionInformation ) } else if (livenessResponse.disconnectionEvent != null) { - this@LivenessWebSocket.webSocket?.close(1000, "Liveness flow completed.") + this@LivenessWebSocket.webSocket?.close(NORMAL_SOCKET_CLOSURE_STATUS_CODE, "Liveness flow completed.") } else { handleWebSocketError(livenessResponse) } @@ -170,7 +168,7 @@ internal class LivenessWebSocket( super.onClosed(webSocket, code, reason) if (reconnectState == ReconnectState.RECONNECTING) { // do nothing; we expected the server to close the connection - } else if (code != 1000 && !clientStoppedSession) { + } else if (code != NORMAL_SOCKET_CLOSURE_STATUS_CODE && !clientStoppedSession) { val faceLivenessException = webSocketError ?: PredictionsException( "An error occurred during the face liveness check.", reason @@ -196,6 +194,12 @@ internal class LivenessWebSocket( } fun start() { + if (reconnectState == ReconnectState.RECONNECTING_AGAIN) { + onErrorReceived.accept(PredictionsException( + "Invalid device time", + "Too many attempts were made to correct device time" + )) + } val userAgent = getUserAgent() val okHttpClient = OkHttpClient.Builder() @@ -463,8 +467,8 @@ internal class LivenessWebSocket( } fun destroy() { - // Close gracefully; 1000 means "normal closure" - webSocket?.close(1000, null) + // Close gracefully + webSocket?.close(NORMAL_SOCKET_CLOSURE_STATUS_CODE, null) } fun adjustedDate(date: Long = Date().time): Long { @@ -472,6 +476,9 @@ internal class LivenessWebSocket( } companion object { + private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000 + private val FIVE_MINUTES = 1000 * 60 * 5 + private val datePattern = "EEE, d MMM yyyy HH:mm:ss z" private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") } } From dcbf84deac06bf5a1381ad6e5bf2d9035dcc3355 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 8 Nov 2023 15:30:06 -0800 Subject: [PATCH 16/17] lint; actually commit test changes --- .../predictions/aws/http/LivenessWebSocket.kt | 15 ++++++++++----- .../predictions/aws/http/LivenessWebSocketTest.kt | 4 ---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 637764575e..607b2de0f8 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -146,7 +146,10 @@ internal class LivenessWebSocket( livenessResponse.serverSessionInformationEvent.sessionInformation ) } else if (livenessResponse.disconnectionEvent != null) { - this@LivenessWebSocket.webSocket?.close(NORMAL_SOCKET_CLOSURE_STATUS_CODE, "Liveness flow completed.") + this@LivenessWebSocket.webSocket?.close( + NORMAL_SOCKET_CLOSURE_STATUS_CODE, + "Liveness flow completed." + ) } else { handleWebSocketError(livenessResponse) } @@ -195,10 +198,12 @@ internal class LivenessWebSocket( fun start() { if (reconnectState == ReconnectState.RECONNECTING_AGAIN) { - onErrorReceived.accept(PredictionsException( - "Invalid device time", - "Too many attempts were made to correct device time" - )) + onErrorReceived.accept( + PredictionsException( + "Invalid device time", + "Too many attempts were made to correct device time" + ) + ) } val userAgent = getUserAgent() diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 220ae3294d..82f68df3be 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -65,7 +65,6 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore @@ -197,14 +196,12 @@ internal class LivenessWebSocketTest { assertTrue(livenessWebSocket.webSocket != null) val originalRequest = livenessWebSocket.webSocket!!.request() - assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) assertTrue( originalRequest.url.queryParameter("X-Amz-Credential")!!.endsWith("//rekognition/aws4_request") ) assertEquals("299", originalRequest.url.queryParameter("X-Amz-Expires")) assertEquals("host", originalRequest.url.queryParameter("X-Amz-SignedHeaders")) assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) - assertNotNull("x-amz-user-agent") } @Test @@ -371,7 +368,6 @@ internal class LivenessWebSocketTest { assertEquals("299", originalRequest.url.queryParameter("X-Amz-Expires")) assertEquals("host", originalRequest.url.queryParameter("X-Amz-SignedHeaders")) assertEquals("AWS4-HMAC-SHA256", originalRequest.url.queryParameter("X-Amz-Algorithm")) - assertNotNull("x-amz-user-agent") } @Test From a2add1603148f133591672b85c23fabaff595530 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 8 Nov 2023 15:46:18 -0800 Subject: [PATCH 17/17] fix unit test --- .../amplifyframework/predictions/aws/http/LivenessWebSocket.kt | 2 +- .../predictions/aws/http/LivenessWebSocketTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 607b2de0f8..ad4c8c1e61 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -483,7 +483,7 @@ internal class LivenessWebSocket( companion object { private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000 private val FIVE_MINUTES = 1000 * 60 * 5 - private val datePattern = "EEE, d MMM yyyy HH:mm:ss z" + @VisibleForTesting val datePattern = "EEE, d MMM yyyy HH:mm:ss z" private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") } } diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index 82f68df3be..42756f0932 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -328,7 +328,7 @@ internal class LivenessWebSocketTest { mockkConstructor(WebSocket::class) val socket: WebSocket = mockk() livenessWebSocket.webSocket = socket - val sdf = SimpleDateFormat(livenessWebSocket.datePattern, Locale.US) + val sdf = SimpleDateFormat(LivenessWebSocket.datePattern, Locale.US) // server responds saying time is actually 1 hour in the future val oneHour = 1000 * 3600