Skip to content

Instantly share code, notes, and snippets.

@realdadfish
Last active January 16, 2019 11:17
Show Gist options
  • Save realdadfish/835582efde516e0cb38932dcf121fb37 to your computer and use it in GitHub Desktop.
Save realdadfish/835582efde516e0cb38932dcf121fb37 to your computer and use it in GitHub Desktop.
Naive persistent cookie jar for OkHttp3
package de.aoksystems.ma.abp.smp.common
import android.content.Context
import android.support.annotation.VisibleForTesting
import de.aoksystems.ma.abp.common.util.InstanceFactory
import de.aoksystems.ma.abp.common.util.TimeProvider
import de.aoksystems.ma.abp.common.util.callbacks.AppCodeResetCallbackRegistry
import io.reactivex.Completable
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
internal class PersistentCookieJar(context: Context, private val timeProvider: TimeProvider) : CookieJar {
private val sharedPreferences by lazy {
context.getSharedPreferences(PREF_FILE_KEY, Context.MODE_PRIVATE)
}
private var cookieStore: Map<String, Cookie> = mapOf()
private fun clearCookies() {
cookieStore = emptyMap()
sharedPreferences.edit().clear().apply()
}
private fun parseCookie(url: String, cookie: String): Cookie {
val parsedUrl = requireNotNull(HttpUrl.parse(url)) { "could not parse URL $url" }
return requireNotNull(Cookie.parse(parsedUrl, cookie)) { "could not parse Cookie $cookie" }
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookies.forEach { newCookie ->
cookieStore = cookieStore.plus(newCookie.key() to newCookie)
}
persistCookies()
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
cookieStore = if (cookieStore.isEmpty()) restoreCookies() else cookieStore
val size = cookieStore.size
if (size == 0) return emptyList()
cookieStore = cookieStore.filter { timeProvider.getCurrentTime() < it.value.expiresAt() }
if (size != cookieStore.size) persistCookies()
return cookieStore.map { it.value }.filter { it.matches(url) }
}
private fun restoreCookies(): Map<String, Cookie> {
val result = mutableMapOf<String, Cookie>()
sharedPreferences.all.entries.forEach { entry ->
result[entry.key] = parseCookie(entry.key, entry.value.toString())
}
return result
}
private fun persistCookies() {
val editor = sharedPreferences.edit()
cookieStore.forEach { entry ->
if (entry.value.persistent()) editor.putString(entry.key, entry.value.toString())
}
editor.apply()
}
internal fun hasCookie(name: String): Boolean = cookieStore.any { it.value.name() == name }
private fun Cookie.key(): String = (if (secure()) "https" else "http") + "://${domain()}${path()}#${name()}"
companion object {
@VisibleForTesting
internal const val PREF_FILE_KEY = "my-pref-file"
}
}
@RunWith(RobolectricTestRunner::class)
class PersistentCookieJarTest {
@Mock
private lateinit var timeProvider: TimeProvider
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
timeProvider.stub {
on { getCurrentTime() } doReturn System.currentTimeMillis()
}
}
@After
fun setup() {
app().getSharedPreferences(PersistentCookieJar.PREF_FILE_KEY, Context.MODE_PRIVATE).edit().clear().apply()
}
@Test
fun shouldDiscardExpiredCookies() {
timeProvider.stub {
on { getCurrentTime() } doReturn EXPIRES_2
}
val sut = sut()
sut.saveFromResponse(URL, listOf(COOKIE_1_1, COOKIE_2))
assert(sut.loadForRequest(URL)).isEqualTo(listOf(COOKIE_1_1))
}
@Test
fun shouldRestoreNonExpiredPersistentCookies() {
timeProvider.stub {
on { getCurrentTime() } doReturn EXPIRES_2 - 1
}
val sut = sut()
sut.saveFromResponse(URL, listOf(COOKIE_1_1, COOKIE_2))
val sut2 = sut()
assert(sut2.loadForRequest(URL)).isEqualTo(listOf(COOKIE_2))
}
@Test
fun shouldOnlyLoadCookiesMatchingPath() {
val sut = sut()
sut.saveFromResponse(URL, listOf(COOKIE_1_1, COOKIE_3))
assert(sut.loadForRequest(URL)).isEqualTo(listOf(COOKIE_1_1))
}
@Test
fun shouldReplaceCookieWithNewValue() {
val sut = sut()
sut.saveFromResponse(URL, listOf(COOKIE_1_1))
sut.saveFromResponse(URL, listOf(COOKIE_1_2))
assert(sut.loadForRequest(URL)).isEqualTo(listOf(COOKIE_1_2))
}
@Test
fun shouldQueryCookieNameFromJar() {
val sut = sut()
sut.saveFromResponse(URL, listOf(COOKIE_1_1))
assert(sut.hasCookie(NAME_1)).isTrue()
assert(sut.hasCookie(NAME_2)).isFalse()
}
private fun sut() = PersistentCookieJar(app(), timeProvider)
companion object {
private val HOST = "www.abc.com"
private val URL =
HttpUrl.Builder()
.host(HOST)
.scheme("https")
.encodedPath("/path")
.build()
private val NAME_1 = "COOKIE_1_NAME"
private val VALUE_1_1 = "COOKIE_1_1_VALUE"
private val COOKIE_1_1 =
Cookie.Builder()
.name(NAME_1)
.value(VALUE_1_1)
.domain(HOST)
.path("/")
.build()
private val VALUE_1_2 = "COOKIE_1_2_VALUE"
private val COOKIE_1_2 =
Cookie.Builder()
.name(NAME_1)
.value(VALUE_1_2)
.domain(HOST)
.path("/")
.build()
private val NAME_2 = "COOKIE_2_NAME"
private val VALUE_2 = "COOKIE_2_VALUE"
private val EXPIRES_2 = 10_000_000L
private val COOKIE_2 =
Cookie.Builder()
.name(NAME_2)
.value(VALUE_2)
.domain(HOST)
.path("/path")
.expiresAt(EXPIRES_2)
.build()
private val NAME_3 = "COOKIE_3_NAME"
private val VALUE_3 = "COOKIE_3_VALUE"
private val COOKIE_3 =
Cookie.Builder()
.name(NAME_3)
.value(VALUE_3)
.domain(HOST)
.path("/otherpath")
.build()
}
}
fun app() = RuntimeEnvironment.application
interface TimeProvider {
fun getCurrentTime(): Long
}
@realdadfish
Copy link
Author

Hacked together, untested, beware!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment