diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt index 9b4be914f9..0bc44144a8 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt @@ -147,7 +147,7 @@ internal class HTTPClient( val fullURL = URL(baseURL, urlPathWithVersion) nonce = if (shouldSignResponse) signingManager.createRandomNonce() else null - val headers = getHeaders(requestHeaders, urlPathWithVersion, refreshETag, nonce) + val headers = getHeaders(requestHeaders, urlPathWithVersion, refreshETag, nonce, shouldSignResponse) val httpRequest = HTTPRequest(fullURL, headers, jsonBody) @@ -237,6 +237,7 @@ internal class HTTPClient( urlPath: String, refreshETag: Boolean, nonce: String?, + shouldSignResponse: Boolean, ): Map { return mapOf( "Content-Type" to "application/json", @@ -252,7 +253,7 @@ internal class HTTPClient( "X-Nonce" to nonce, ) .plus(authenticationHeaders) - .plus(eTagManager.getETagHeaders(urlPath, refreshETag)) + .plus(eTagManager.getETagHeaders(urlPath, shouldSignResponse, refreshETag)) .filterNotNullValues() } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/networking/ETagManager.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/networking/ETagManager.kt index 3099b92e24..ce544833ae 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/networking/ETagManager.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/networking/ETagManager.kt @@ -55,9 +55,11 @@ internal class ETagManager( internal fun getETagHeaders( path: String, + verificationRequested: Boolean, refreshETag: Boolean = false, ): Map { - val eTagData = if (refreshETag) null else getETagData(path) + val storedResult = if (refreshETag) null else getStoredResultSavedInSharedPreferences(path) + val eTagData = storedResult?.eTagData?.takeIf { shouldUseETag(storedResult, verificationRequested) } return mapOf( HTTPRequest.ETAG_HEADER_NAME to eTagData?.eTag.orEmpty(), HTTPRequest.ETAG_LAST_REFRESH_NAME to eTagData?.lastRefreshTime?.time?.toString(), @@ -146,10 +148,6 @@ internal class ETagManager( } } - private fun getETagData(path: String): ETagData? { - return getStoredResultSavedInSharedPreferences(path)?.eTagData - } - private fun shouldStoreBackendResult(resultFromBackend: HTTPResult): Boolean { val responseCode = resultFromBackend.responseCode return responseCode != RCHTTPStatusCodes.NOT_MODIFIED && @@ -157,6 +155,15 @@ internal class ETagManager( resultFromBackend.verificationResult != VerificationResult.FAILED } + private fun shouldUseETag(storedResult: HTTPResultWithETag, verificationRequested: Boolean): Boolean { + return when (storedResult.httpResult.verificationResult) { + VerificationResult.VERIFIED -> true + VerificationResult.NOT_REQUESTED -> !verificationRequested + // Should never happen since we don't store these verification results in the cache + VerificationResult.FAILED, VerificationResult.VERIFIED_ON_DEVICE -> false + } + } + companion object { fun initializeSharedPreferences(context: Context): SharedPreferences = context.getSharedPreferences( diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/BaseHTTPClientTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/BaseHTTPClientTest.kt index f076806181..63c66064fa 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/BaseHTTPClientTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/BaseHTTPClientTest.kt @@ -39,7 +39,7 @@ internal abstract class BaseHTTPClientTest { protected val mockETagManager = mockk().also { every { - it.getETagHeaders(any(), any()) + it.getETagHeaders(any(), any(), any()) } answers { mapOf(HTTPRequest.ETAG_HEADER_NAME to "") } diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientTest.kt index ba0e90e407..85fa75a13b 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientTest.kt @@ -195,7 +195,7 @@ internal class HTTPClientTest: BaseHTTPClientTest() { val endpoint = Endpoint.LogIn every { - mockETagManager.getETagHeaders(any(), any()) + mockETagManager.getETagHeaders(any(), any(), any()) } answers { mapOf( HTTPRequest.ETAG_HEADER_NAME to "mock-etag", @@ -222,7 +222,7 @@ internal class HTTPClientTest: BaseHTTPClientTest() { val endpoint = Endpoint.LogIn every { - mockETagManager.getETagHeaders(any(), any()) + mockETagManager.getETagHeaders(any(), any(), any()) } answers { mapOf( HTTPRequest.ETAG_HEADER_NAME to "mock-etag", @@ -365,10 +365,10 @@ internal class HTTPClientTest: BaseHTTPClientTest() { server.takeRequest() verify(exactly = 1) { - mockETagManager.getETagHeaders(any(), false) + mockETagManager.getETagHeaders(any(), any(), refreshETag = false) } verify(exactly = 1) { - mockETagManager.getETagHeaders(any(), true) + mockETagManager.getETagHeaders(any(), any(), refreshETag = true) } assertThat(result.payload).isEqualTo(expectedResult.payload) assertThat(result.responseCode).isEqualTo(expectedResult.responseCode) diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/networking/ETagManagerTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/networking/ETagManagerTest.kt index 2f9574f3a9..9d78da3a31 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/networking/ETagManagerTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/networking/ETagManagerTest.kt @@ -56,7 +56,7 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = null, path = path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] assertThat(eTagHeader).isNotNull assertThat(eTagHeader).isBlank @@ -67,7 +67,7 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = null, path = path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) val lastRefreshTimeHeader = eTagHeaders[HTTPRequest.ETAG_LAST_REFRESH_NAME] assertThat(lastRefreshTimeHeader).isNull() } @@ -77,7 +77,7 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = "etag", expectedLastRefreshTime = null, path = path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] assertThat(eTagHeader).isEqualTo("etag") @@ -91,7 +91,7 @@ class ETagManagerTest { val expectedETag = "etag" mockCachedHTTPResult(expectedETag, path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] assertThat(eTagHeader).isNotNull assertThat(eTagHeader).isEqualTo(expectedETag) @@ -102,7 +102,7 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = "etag", expectedLastRefreshTime = testDate, path = path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) val lastRefreshTimeHeader = eTagHeaders[HTTPRequest.ETAG_LAST_REFRESH_NAME] assertThat(lastRefreshTimeHeader).isEqualTo("1675954145") } @@ -112,10 +112,86 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = "etag", expectedLastRefreshTime = testDate, path = path) - val eTagHeaders = underTest.getETagHeaders(path) + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) assertThat(eTagHeaders.size).isEqualTo(2) } + // region ETag headers usage verification tests + + @Test + fun `ETag headers are added if cached result is verified and verification not requested`() { + val path = "/v1/subscribers/appUserID" + val expectedETag = "etag" + mockCachedHTTPResult( + expectedETag, + path, + httpResult = HTTPResult.createResult(origin = HTTPResult.Origin.CACHE, verificationResult = VERIFIED), + ) + + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) + val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] + assertThat(eTagHeader).isEqualTo(expectedETag) + } + + @Test + fun `ETag headers are added if cached result is not requested and verification not requested`() { + val path = "/v1/subscribers/appUserID" + val expectedETag = "etag" + mockCachedHTTPResult( + expectedETag, + path, + httpResult = HTTPResult.createResult(origin = HTTPResult.Origin.CACHE, verificationResult = NOT_REQUESTED), + ) + + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) + val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] + assertThat(eTagHeader).isEqualTo(expectedETag) + } + + @Test + fun `ETag headers are not added if cached result is not requested and verification requested`() { + val path = "/v1/subscribers/appUserID" + mockCachedHTTPResult( + "etag", + path, + httpResult = HTTPResult.createResult(origin = HTTPResult.Origin.CACHE, verificationResult = NOT_REQUESTED), + ) + + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = true) + val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] + assertThat(eTagHeader).isEqualTo("") + } + + @Test + fun `ETag headers are not added if cached result errored`() { + val path = "/v1/subscribers/appUserID" + mockCachedHTTPResult( + "etag", + path, + httpResult = HTTPResult.createResult(origin = HTTPResult.Origin.CACHE, verificationResult = FAILED), + ) + + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) + val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] + assertThat(eTagHeader).isEqualTo("") + } + + @Test + fun `ETag headers are not added if cached result verified on device`() { + val path = "/v1/subscribers/appUserID" + mockCachedHTTPResult( + "etag", + path, + httpResult = HTTPResult.createResult(origin = HTTPResult.Origin.CACHE, verificationResult = VERIFIED_ON_DEVICE), + ) + + val eTagHeaders = underTest.getETagHeaders(path, verificationRequested = false) + val eTagHeader = eTagHeaders[HTTPRequest.ETAG_HEADER_NAME] + assertThat(eTagHeader).isEqualTo("") + } + + // endregion ETag headers usage verification tests + @Test fun `If response code is 304, cached version should be used`() { val shouldUse = underTest.shouldUseCachedVersion(RCHTTPStatusCodes.NOT_MODIFIED) @@ -256,7 +332,7 @@ class ETagManagerTest { val path = "/v1/subscribers/appUserID" mockCachedHTTPResult(expectedETag = null, path = path) - val requestWithETagHeader = underTest.getETagHeaders(path, refreshETag = true) + val requestWithETagHeader = underTest.getETagHeaders(path, verificationRequested = false, refreshETag = true) val eTagHeader = requestWithETagHeader[HTTPRequest.ETAG_HEADER_NAME] assertThat(eTagHeader).isNotNull assertThat(eTagHeader).isBlank