mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Minor fixes to sync protocol
This commit is contained in:
@@ -24,11 +24,10 @@ interface AbstractSyncServer {
|
||||
* Generates and returns a new sync key, which can be used to store and retrive
|
||||
* data.
|
||||
*
|
||||
* @throws RegistrationUnavailableException If key cannot be generated at this
|
||||
* time, for example, due to insufficient server resources or temporary
|
||||
* maintenance.
|
||||
* @throws ServiceUnavailable If key cannot be generated at this time, for example,
|
||||
* due to insufficient server resources, temporary server maintenance or network problems.
|
||||
*/
|
||||
fun register(): String
|
||||
suspend fun register(): String
|
||||
|
||||
/**
|
||||
* Replaces data for a given sync key.
|
||||
@@ -36,13 +35,26 @@ interface AbstractSyncServer {
|
||||
* @throws KeyNotFoundException If key is not found
|
||||
* @throws EditConflictException If the version of the data provided is not
|
||||
* exactly the current data version plus one.
|
||||
* @throws ServiceUnavailable If data cannot be put at this time, for example, due
|
||||
* to insufficient server resources or network problems.
|
||||
*/
|
||||
fun put(key: String, newData: SyncData)
|
||||
suspend fun put(key: String, newData: SyncData)
|
||||
|
||||
/**
|
||||
* Returns data for a given sync key.
|
||||
*
|
||||
* @throws KeyNotFoundException If key is not found
|
||||
* @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due
|
||||
* to insufficient server resources or network problems.
|
||||
*/
|
||||
fun get(key: String): SyncData
|
||||
}
|
||||
suspend fun getData(key: String): SyncData
|
||||
|
||||
/**
|
||||
* Returns the current data version for the given key
|
||||
*
|
||||
* @throws KeyNotFoundException If key is not found
|
||||
* @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due
|
||||
* to insufficient server resources or network problems.
|
||||
*/
|
||||
suspend fun getDataVersion(key: String): Long
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlin.streams.*
|
||||
class MemorySyncServer : AbstractSyncServer {
|
||||
private val db = mutableMapOf<String, SyncData>()
|
||||
|
||||
override fun register(): String {
|
||||
override suspend fun register(): String {
|
||||
synchronized(db) {
|
||||
val key = generateKey()
|
||||
db[key] = SyncData(0, "")
|
||||
@@ -36,7 +36,7 @@ class MemorySyncServer : AbstractSyncServer {
|
||||
}
|
||||
}
|
||||
|
||||
override fun put(key: String, newData: SyncData) {
|
||||
override suspend fun put(key: String, newData: SyncData) {
|
||||
synchronized(db) {
|
||||
if (!db.containsKey(key)) {
|
||||
throw KeyNotFoundException()
|
||||
@@ -49,7 +49,7 @@ class MemorySyncServer : AbstractSyncServer {
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(key: String): SyncData {
|
||||
override suspend fun getData(key: String): SyncData {
|
||||
synchronized(db) {
|
||||
if (!db.containsKey(key)) {
|
||||
throw KeyNotFoundException()
|
||||
@@ -58,6 +58,15 @@ class MemorySyncServer : AbstractSyncServer {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDataVersion(key: String): Long {
|
||||
synchronized(db) {
|
||||
if (!db.containsKey(key)) {
|
||||
throw KeyNotFoundException()
|
||||
}
|
||||
return db.getValue(key).version
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateKey(): String {
|
||||
val chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
while (true) {
|
||||
|
||||
@@ -22,9 +22,14 @@ package org.isoron.uhabits.sync
|
||||
import com.fasterxml.jackson.databind.*
|
||||
|
||||
data class SyncData(
|
||||
val version: Int,
|
||||
val content: String,
|
||||
val version: Long,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class RegisterReponse(val key: String)
|
||||
|
||||
data class GetDataVersionResponse(val version: Long)
|
||||
|
||||
val defaultMapper = ObjectMapper()
|
||||
fun SyncData.toJson(): String = defaultMapper.writeValueAsString(this)
|
||||
fun GetDataVersionResponse.toJson(): String = defaultMapper.writeValueAsString(this)
|
||||
@@ -19,13 +19,10 @@
|
||||
|
||||
package org.isoron.uhabits.sync
|
||||
|
||||
/**
|
||||
* Generic class for all exceptions thrown by SyncServer.
|
||||
*/
|
||||
open class SyncException: RuntimeException()
|
||||
|
||||
class KeyNotFoundException: SyncException()
|
||||
|
||||
class RegistrationUnavailableException: SyncException()
|
||||
class ServiceUnavailable: SyncException()
|
||||
|
||||
class EditConflictException: SyncException()
|
||||
@@ -29,8 +29,8 @@ fun Routing.registration(app: SyncApplication) {
|
||||
post("/register") {
|
||||
try {
|
||||
val key = app.server.register()
|
||||
call.respond(HttpStatusCode.OK, mapOf("key" to key))
|
||||
} catch (e: RegistrationUnavailableException) {
|
||||
call.respond(HttpStatusCode.OK, RegisterReponse(key))
|
||||
} catch (e: ServiceUnavailable) {
|
||||
call.respond(HttpStatusCode.ServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ fun Routing.storage(app: SyncApplication) {
|
||||
get {
|
||||
val key = call.parameters["key"]!!
|
||||
try {
|
||||
val data = app.server.get(key)
|
||||
val data = app.server.getData(key)
|
||||
call.respond(HttpStatusCode.OK, data)
|
||||
} catch(e: KeyNotFoundException) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
@@ -46,15 +46,14 @@ fun Routing.storage(app: SyncApplication) {
|
||||
} catch (e: KeyNotFoundException) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
} catch (e: EditConflictException) {
|
||||
val currData = app.server.get(key)
|
||||
call.respond(HttpStatusCode.Conflict, currData)
|
||||
call.respond(HttpStatusCode.Conflict)
|
||||
}
|
||||
}
|
||||
get("version") {
|
||||
val key = call.parameters["key"]!!
|
||||
try {
|
||||
val data = app.server.get(key)
|
||||
call.respond(HttpStatusCode.OK, data.version)
|
||||
val version = app.server.getDataVersion(key)
|
||||
call.respond(HttpStatusCode.OK, GetDataVersionResponse(version))
|
||||
} catch(e: KeyNotFoundException) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
@@ -19,33 +19,34 @@
|
||||
|
||||
package org.isoron.uhabits.sync
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
|
||||
class MemorySyncServerTest {
|
||||
|
||||
private val server = MemorySyncServer()
|
||||
private val key = server.register()
|
||||
private val key = runBlocking { server.register() }
|
||||
|
||||
@Test
|
||||
fun testUsage() {
|
||||
fun testUsage(): Unit = runBlocking {
|
||||
val data0 = SyncData(0, "")
|
||||
assertEquals(server.get(key), data0)
|
||||
assertEquals(server.getData(key), data0)
|
||||
|
||||
val data1 = SyncData(1, "Hello world")
|
||||
server.put(key, data1)
|
||||
assertEquals(server.get(key), data1)
|
||||
assertEquals(server.getData(key), data1)
|
||||
|
||||
val data2 = SyncData(2, "Hello new world")
|
||||
server.put(key, data2)
|
||||
assertEquals(server.get(key), data2)
|
||||
assertEquals(server.getData(key), data2)
|
||||
|
||||
assertFailsWith<EditConflictException> {
|
||||
server.put(key, data2)
|
||||
}
|
||||
|
||||
assertFailsWith<KeyNotFoundException> {
|
||||
server.get("INVALID")
|
||||
server.getData("INVALID")
|
||||
}
|
||||
|
||||
assertFailsWith<KeyNotFoundException> {
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.sync.app
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.testing.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.isoron.uhabits.sync.*
|
||||
import org.junit.Test
|
||||
import org.mockito.*
|
||||
@@ -29,7 +30,7 @@ import kotlin.test.*
|
||||
|
||||
class RegistrationModuleTest : BaseApplicationTest() {
|
||||
@Test
|
||||
fun `when register succeeds should return generated key`() {
|
||||
fun `when register succeeds should return generated key`():Unit = runBlocking {
|
||||
`when`(server.register()).thenReturn("ABCDEF")
|
||||
withTestApplication(app()) {
|
||||
val call = handleRequest(HttpMethod.Post, "/register")
|
||||
@@ -39,8 +40,8 @@ class RegistrationModuleTest : BaseApplicationTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when registration is unavailable should return 503`() {
|
||||
`when`(server.register()).thenThrow(RegistrationUnavailableException())
|
||||
fun `when registration is unavailable should return 503`():Unit = runBlocking {
|
||||
`when`(server.register()).thenThrow(ServiceUnavailable())
|
||||
withTestApplication(app()) {
|
||||
val call = handleRequest(HttpMethod.Post, "/register")
|
||||
assertEquals(HttpStatusCode.ServiceUnavailable, call.response.status())
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.sync.app
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.testing.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.isoron.uhabits.sync.*
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.*
|
||||
@@ -31,8 +32,8 @@ class StorageModuleTest : BaseApplicationTest() {
|
||||
private val data2 = SyncData(2, "Hello new world")
|
||||
|
||||
@Test
|
||||
fun `when get succeeds should return data`() {
|
||||
`when`(server.get("k1")).thenReturn(data1)
|
||||
fun `when get succeeds should return data`(): Unit = runBlocking {
|
||||
`when`(server.getData("k1")).thenReturn(data1)
|
||||
withTestApplication(app()) {
|
||||
handleGet("/db/k1").apply {
|
||||
assertEquals(HttpStatusCode.OK, response.status())
|
||||
@@ -42,19 +43,19 @@ class StorageModuleTest : BaseApplicationTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get version succeeds should return version`() {
|
||||
`when`(server.get("k1")).thenReturn(data1)
|
||||
fun `when get version succeeds should return version`(): Unit = runBlocking {
|
||||
`when`(server.getDataVersion("k1")).thenReturn(30)
|
||||
withTestApplication(app()) {
|
||||
handleGet("/db/k1/version").apply {
|
||||
assertEquals(HttpStatusCode.OK, response.status())
|
||||
assertEquals("1", response.content)
|
||||
assertEquals(GetDataVersionResponse(30).toJson(), response.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get with invalid key should return 404`() {
|
||||
`when`(server.get("k1")).thenThrow(KeyNotFoundException())
|
||||
fun `when get with invalid key should return 404`(): Unit = runBlocking {
|
||||
`when`(server.getData("k1")).thenThrow(KeyNotFoundException())
|
||||
withTestApplication(app()) {
|
||||
handleGet("/db/k1").apply {
|
||||
assertEquals(HttpStatusCode.NotFound, response.status())
|
||||
@@ -64,17 +65,19 @@ class StorageModuleTest : BaseApplicationTest() {
|
||||
|
||||
|
||||
@Test
|
||||
fun `when put succeeds should return OK`() {
|
||||
fun `when put succeeds should return OK`(): Unit = runBlocking {
|
||||
withTestApplication(app()) {
|
||||
handlePut("/db/k1", data1).apply {
|
||||
assertEquals(HttpStatusCode.OK, response.status())
|
||||
verify(server).put("k1", data1)
|
||||
runBlocking {
|
||||
assertEquals(HttpStatusCode.OK, response.status())
|
||||
verify(server).put("k1", data1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when put with invalid key should return 404`() {
|
||||
fun `when put with invalid key should return 404`(): Unit = runBlocking {
|
||||
`when`(server.put("k1", data1)).thenThrow(KeyNotFoundException())
|
||||
withTestApplication(app()) {
|
||||
handlePut("/db/k1", data1).apply {
|
||||
@@ -84,13 +87,12 @@ class StorageModuleTest : BaseApplicationTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when put with invalid version should return 409 and current data`() {
|
||||
fun `when put with invalid version should return 409 and current data`(): Unit = runBlocking {
|
||||
`when`(server.put("k1", data1)).thenThrow(EditConflictException())
|
||||
`when`(server.get("k1")).thenReturn(data2)
|
||||
`when`(server.getData("k1")).thenReturn(data2)
|
||||
withTestApplication(app()) {
|
||||
handlePut("/db/k1", data1).apply {
|
||||
assertEquals(HttpStatusCode.Conflict, response.status())
|
||||
assertEquals(data2.toJson(), response.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user