diff --git a/.idea/runConfigurations/Backend____Jetty.xml b/.idea/runConfigurations/Backend____Jetty.xml
deleted file mode 100644
index 17fe544..0000000
--- a/.idea/runConfigurations/Backend____Jetty.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/backend/build.gradle b/backend/build.gradle
index 6097f03..d854fb4 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -1,3 +1,7 @@
+plugins {
+ id "jacoco"
+}
+
group = 'org.jetbrains.demo.thinkter'
version = '0.0.1-SNAPSHOT'
@@ -5,6 +9,7 @@ apply plugin: 'kotlin'
apply plugin: 'application'
dependencies {
+
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@@ -18,6 +23,8 @@ dependencies {
testCompile "org.jetbrains.ktor:ktor-test-host:$ktor_version"
testCompile "org.jsoup:jsoup:1.9.1"
+ testCompile "io.mockk:mockk:1.9.1.kotlin12"
+
compile "org.jetbrains.ktor:ktor-jetty:$ktor_version"
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
}
@@ -42,3 +49,12 @@ kotlin {
}
mainClassName = 'org.jetbrains.ktor.jetty.DevelopmentHost'
+
+jacocoTestReport {
+ reports {
+ xml.enabled true
+ html.enabled true
+ }
+}
+
+check.dependsOn jacocoTestReport
diff --git a/backend/src/org/jetbrains/demo/thinkter/Register.kt b/backend/src/org/jetbrains/demo/thinkter/Register.kt
index 0752763..db70d0e 100644
--- a/backend/src/org/jetbrains/demo/thinkter/Register.kt
+++ b/backend/src/org/jetbrains/demo/thinkter/Register.kt
@@ -11,8 +11,6 @@ import org.jetbrains.ktor.util.*
fun Route.register(dao: ThinkterStorage, hashFunction: (String) -> String) {
post { form ->
- val vm = call.request.content.get()
-
val user = call.sessionOrNull()?.let { dao.user(it.userId) }
if (user != null) {
call.redirect(LoginResponse(user))
@@ -40,6 +38,7 @@ fun Route.register(dao: ThinkterStorage, hashFunction: (String) -> String) {
application.environment.log.error("Failed to register user", e)
call.respond(LoginResponse(error = "Failed to register"))
}
+ return@post
}
call.session(Session(newUser.userId))
diff --git a/backend/test/org/jetbrains/demo/thinkter/ApplicationPageTest.kt b/backend/test/org/jetbrains/demo/thinkter/ApplicationPageTest.kt
new file mode 100644
index 0000000..bfb7c40
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/ApplicationPageTest.kt
@@ -0,0 +1,35 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import kotlinx.coroutines.experimental.runBlocking
+import org.jetbrains.ktor.application.ApplicationCall
+import org.jetbrains.ktor.cio.ByteBufferWriteChannel
+import org.jetbrains.ktor.html.HtmlContent
+import org.jetbrains.ktor.html.respondHtmlTemplate
+import org.junit.Test
+import java.nio.charset.Charset
+
+class ApplicationPageTest {
+ val appCall = mockk()
+
+ @Test
+ fun testRenderHTML() {
+ coEvery { appCall.respond(any()) } just Runs
+
+ runBlocking {
+ appCall.respondHtmlTemplate(ApplicationPage()) {
+ caption { +"caption" }
+ }
+ }
+
+ coVerify {
+ appCall.respond(coAssert {
+ val channel = ByteBufferWriteChannel()
+ it.writeTo(channel)
+ val html = channel.toString(Charset.defaultCharset())
+ html.contains("caption") && html.contains("yui.yahooapis.com")
+ })
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/backend/test/org/jetbrains/demo/thinkter/Common.kt b/backend/test/org/jetbrains/demo/thinkter/Common.kt
new file mode 100644
index 0000000..2b7130c
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/Common.kt
@@ -0,0 +1,96 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.Thought
+import org.jetbrains.demo.thinkter.model.User
+import org.jetbrains.ktor.application.ApplicationCall
+import org.jetbrains.ktor.http.HttpHeaders
+import org.jetbrains.ktor.http.HttpStatusCode
+import org.jetbrains.ktor.request.host
+import org.jetbrains.ktor.sessions.SessionConfig
+import org.jetbrains.ktor.util.AttributeKey
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+
+fun MockKMatcherScope.sessionMatcher(): AttributeKey =
+ match { it.name == "Session" }
+
+fun MockKMatcherScope.sessionConfigMatcher(): AttributeKey> =
+ match { it.name == "SessionConfig" }
+
+
+fun ApplicationCall.mockSessionReturningUser(dao: ThinkterStorage) {
+ every { attributes.contains(sessionMatcher()) } returns true
+
+ every {
+ attributes
+ .get(sessionMatcher())
+ } returns Session("userId")
+
+ every { dao.user("userId") } returns User("userId",
+ "email",
+ "User",
+ "pwd")
+}
+
+
+fun ApplicationCall.mockSessionReturningNothing() {
+ every { attributes.contains(sessionMatcher()) } returns false
+}
+
+
+fun ApplicationCall.checkForbiddenIfSesionReturningNothing(handle: () -> Unit) {
+ mockSessionReturningNothing()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(HttpStatusCode.Forbidden) }
+}
+
+fun ApplicationCall.mockHostReferrerHash(hash: (String) -> String) {
+ every { request.host() } returns "host"
+
+ every { request.headers[HttpHeaders.Referrer] } returns "http://abc/referrer"
+
+ every { hash(any()) } answers { firstArg().reversed() }
+}
+
+
+fun mockGetThought(dao: ThinkterStorage, ts: Long) {
+ every {
+ dao.getThought(any())
+ } answers { Thought(firstArg(),
+ "userId",
+ "text",
+ formatDate(ts + firstArg()),
+ null) }
+}
+
+private fun formatDate(date: Long): String {
+ return Instant.ofEpochMilli(date)
+ .atZone(ZoneId.systemDefault())
+ .toOffsetDateTime()
+ .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+}
+
+
+fun mockUser(dao: ThinkterStorage, pwdHash: String? = null): User {
+ val user = User("abcdef", "abc@def", "Abc Def", pwdHash ?: "")
+ every { dao.user("abcdef", pwdHash) } returns user
+ return user
+}
+
+fun ApplicationCall.mockPutSession() {
+ every {
+ attributes
+ .get(sessionConfigMatcher())
+ .sessionType
+ } returns Session::class
+
+ every { attributes.put(sessionMatcher(), any()) } just Runs
+}
+
diff --git a/backend/test/org/jetbrains/demo/thinkter/DeleteKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/DeleteKtTest.kt
new file mode 100644
index 0000000..d0197b5
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/DeleteKtTest.kt
@@ -0,0 +1,104 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.PostThoughtToken
+import org.jetbrains.demo.thinkter.model.RpcData
+import org.jetbrains.demo.thinkter.model.Thought
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class DeleteKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getThoughtDelete = RouteBlockSlot()
+ val postThoughtDelete = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getThoughtDelete)
+ }
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postThoughtDelete)
+ }
+ }
+ }
+
+ route.delete(dao, hash)
+ }
+
+ @Test
+ fun testGetThoughtDeleteOk() {
+ val data = ThoughtDelete(1, System.currentTimeMillis() - 6000, "abc")
+ getThoughtDelete.invokeBlock(locations, data) { handle ->
+ mockSessionReturningUser(dao)
+ mockHostReferrerHash(hash)
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.user == "userId" &&
+ it.code.contains("cba:tsoh:dIresu")
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetThoughtDeleteNotLoggedIn() {
+ val data = ThoughtDelete(0, 0, "abc")
+ getThoughtDelete.invokeBlock(locations, data) { handle ->
+ checkForbiddenIfSesionReturningNothing(handle)
+ }
+ }
+
+ @Test
+ fun testPostThoughtDeleteOk() {
+ val ts = System.currentTimeMillis() - 6000
+ val data = ThoughtDelete(1, ts, "cba:tsoh:dIresu:" + ts.toString().reversed())
+ postThoughtDelete.invokeBlock(locations, data) { handle ->
+ mockSessionReturningUser(dao)
+ mockHostReferrerHash(hash)
+ mockGetThought(dao, ts)
+
+ every {
+ dao.deleteThought(1)
+ } just Runs
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(ofType(RpcData::class))
+ }
+ }
+ }
+
+ @Test
+ fun testPostThoughtDeleteNotLoggedIn() {
+ val data = ThoughtDelete(1, 0, "abc")
+ val ts = System.currentTimeMillis()
+ postThoughtDelete.invokeBlock(locations, data) { handle ->
+ every {
+ dao.getThought(1)
+ } answers { Thought(1, "userId", "text", ts.toString(), null) }
+
+ checkForbiddenIfSesionReturningNothing(handle)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/backend/test/org/jetbrains/demo/thinkter/IndexKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/IndexKtTest.kt
new file mode 100644
index 0000000..06b190d
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/IndexKtTest.kt
@@ -0,0 +1,146 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.IndexResponse
+import org.jetbrains.demo.thinkter.model.PollResponse
+import org.jetbrains.ktor.cio.ByteBufferWriteChannel
+import org.jetbrains.ktor.html.HtmlContent
+import org.jetbrains.ktor.http.HttpHeaders
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpHeaderRouteSelector
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+import java.nio.charset.Charset
+
+class IndexKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val locations = mockk()
+
+ val getHtmlIndex = RouteBlockSlot()
+ val getJsonIndex = RouteBlockSlot()
+ val getJsonPoll = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockSelect(HttpHeaderRouteSelector(HttpHeaders.Accept, "text/html")) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getHtmlIndex)
+ }
+ }
+ }
+ mockSelect(HttpHeaderRouteSelector(HttpHeaders.Accept, "application/json")) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getJsonIndex)
+ }
+ }
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getJsonPoll)
+ }
+ }
+ }
+ }
+
+ route.index(dao)
+
+ }
+
+ @Test
+ fun testGetIndexHtml() {
+ getHtmlIndex.invokeBlock(locations, Index()) { handle ->
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(coAssert {
+ val channel = ByteBufferWriteChannel()
+ it.writeTo(channel)
+ val html = channel.toString(Charset.defaultCharset())
+ html.contains("Thinkter")
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetIndexJson() {
+ getJsonIndex.invokeBlock(locations, Index()) { handle ->
+ mockSessionReturningUser(dao)
+ mockGetThought(dao, 0)
+
+ every { dao.top(10) } returns (1..10).toList()
+
+ every { dao.latest(10) } returns (1..10).toList()
+
+ coEvery { respond(any()) } just Runs
+
+ every { response.pipeline.intercept(any(), any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert(msg = "response should have top and latest with ids from one to ten") {
+ val oneToTen = (1..10).toList()
+
+ it.top.map { it.id }.containsAll(oneToTen)
+ && it.latest.map { it.id }.containsAll(oneToTen)
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetPollJsonBlank() {
+ getJsonPoll.invokeBlock(locations, Poll("")) { handle ->
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert { it.count == "0" })
+ }
+ }
+ }
+
+ @Test
+ fun testGetPollJsonOne() {
+ checkPoll("9", "1")
+ }
+
+ @Test
+ fun testGetPollJsonFive() {
+ checkPoll("5", "5")
+ }
+
+ @Test
+ fun testGetPollJsonTenPlus() {
+ checkPoll("0", "10+")
+ }
+
+ private fun checkPoll(pollTime: String, responseCount: String) {
+ getJsonPoll.invokeBlock(locations, Poll(pollTime)) { handle ->
+ mockGetThought(dao, 0)
+
+ every { dao.latest(10) } returns (1..10).toList()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert { it.count == responseCount })
+ }
+ }
+ }
+
+}
+
diff --git a/backend/test/org/jetbrains/demo/thinkter/LoginKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/LoginKtTest.kt
new file mode 100644
index 0000000..346d0e3
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/LoginKtTest.kt
@@ -0,0 +1,186 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.LoginResponse
+import org.jetbrains.demo.thinkter.model.User
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.http.HttpStatusCode
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class LoginKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getLogin = RouteBlockSlot()
+ val postLogin = RouteBlockSlot()
+ val postLogout = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getLogin)
+ }
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postLogin)
+ }
+ }
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postLogout)
+ }
+ }
+ }
+
+ route.login(dao, hash)
+
+ }
+
+ @Test
+ fun testGetLoginOk() {
+ val user = User("userId",
+ "email",
+ "display",
+ "pwd")
+ every {
+ dao.user("userId", any())
+ } returns user
+
+ getLogin.invokeBlock(locations,
+ Login("abc",
+ "def",
+ "ghi")) { handle ->
+ every { attributes.contains(sessionMatcher()) } returns true
+
+ every {
+ attributes
+ .get(sessionMatcher())
+ } returns Session("userId")
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(LoginResponse(user)) }
+ }
+ }
+
+ @Test
+ fun testGetLoginNotLoggedIn() {
+ getLogin.invokeBlock(locations,
+ Login("abc",
+ "def",
+ "ghi")) { handle ->
+ every { attributes.contains(sessionMatcher()) } returns false
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(HttpStatusCode.Forbidden) }
+ }
+ }
+
+ @Test
+ fun testPostLoginOk() {
+ postLogin.invokeBlock(locations,
+ Login("abcdef",
+ "ghiklm")) { handle ->
+
+ every { hash.invoke("ghiklm") } returns "mlkihg"
+
+ val user = mockUser(dao, "mlkihg")
+
+ mockPutSession()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(LoginResponse(user)) }
+
+ coVerify { attributes.put(sessionMatcher(), Session("abcdef")) }
+ }
+ }
+
+ @Test
+ fun testPostLoginShortUsername() {
+ postLogin.invokeBlock(locations,
+ Login("abc",
+ "defghi")) { handle ->
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(LoginResponse(error = "Invalid username or password")) }
+ }
+ }
+
+ @Test
+ fun testPostLoginShortPassword() {
+ postLogin.invokeBlock(locations,
+ Login("abcdef",
+ "ghi")) { handle ->
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(LoginResponse(error = "Invalid username or password")) }
+ }
+ }
+
+ @Test
+ fun testPostLoginWrongUsername() {
+ postLogin.invokeBlock(locations,
+ Login("#!$%#$$@#",
+ "defghi")) { handle ->
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(LoginResponse(error = "Invalid username or password")) }
+ }
+ }
+
+ @Test
+ fun testPostLogoutOk() {
+ postLogout.invokeBlock(locations,
+ Logout()) { handle ->
+
+ every { hash.invoke("ghiklm") } returns "mlkihg"
+
+ every {
+ dao.user("abcdef", "mlkihg")
+ } returns User("abcdef",
+ "abc@def",
+ "Abc Def",
+ "mlkihg")
+
+ every {
+ attributes
+ .getOrNull(sessionConfigMatcher())
+ } returns null
+
+ every { attributes.remove(sessionMatcher()) } just Runs
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(HttpStatusCode.OK) }
+
+ verify { attributes.remove(sessionMatcher()) }
+ }
+ }
+}
diff --git a/backend/test/org/jetbrains/demo/thinkter/PostThoughtKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/PostThoughtKtTest.kt
new file mode 100644
index 0000000..ff13962
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/PostThoughtKtTest.kt
@@ -0,0 +1,98 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.PostThoughtResult
+import org.jetbrains.demo.thinkter.model.PostThoughtToken
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class PostThoughtKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getPostThought = RouteBlockSlot()
+ val postPostThought = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getPostThought)
+ }
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postPostThought)
+ }
+ }
+ }
+
+ route.postThought(dao, hash)
+ }
+
+ @Test
+ fun testGetPostThoughtOk() {
+ getPostThought.invokeBlock(locations, PostThought()) { handle ->
+ mockSessionReturningUser(dao)
+ mockHostReferrerHash(hash)
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.user == "userId" &&
+ it.code.contains("cba:tsoh:dIresu")
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetPostThoughtNotLoggedIn() {
+ getPostThought.invokeBlock(locations, PostThought()) { handle ->
+ checkForbiddenIfSesionReturningNothing(handle)
+ }
+ }
+
+ @Test
+ fun testPostPostThoughtOk() {
+ val ts = System.currentTimeMillis() - 6000
+ val data = PostThought("text", ts, "cba:tsoh:dIresu:" + ts.toString().reversed(), null)
+ postPostThought.invokeBlock(locations, data) { handle ->
+ mockSessionReturningUser(dao)
+ mockHostReferrerHash(hash)
+ mockGetThought(dao, ts)
+
+ every {
+ dao.createThought("userId", "text", any(), any())
+ } returns 1
+
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.thought.id == 1 &&
+ it.thought.text == "text"
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testPostPostThoughtNotLoggedIn() {
+ postPostThought.invokeBlock(locations, PostThought()) { handle ->
+ checkForbiddenIfSesionReturningNothing(handle)
+ }
+ }
+}
diff --git a/backend/test/org/jetbrains/demo/thinkter/RegisterKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/RegisterKtTest.kt
new file mode 100644
index 0000000..28fabf2
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/RegisterKtTest.kt
@@ -0,0 +1,233 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.LoginResponse
+import org.jetbrains.demo.thinkter.model.User
+import org.jetbrains.ktor.application.ApplicationFeature
+import org.jetbrains.ktor.http.HttpHeaders
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.http.HttpStatusCode
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class RegisterKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getRegister = RouteBlockSlot()
+ val postRegister = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postRegister)
+ }
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getRegister)
+ }
+ }
+ }
+
+ route.register(dao, hash)
+ }
+
+ @Test
+ fun testPostRegisterOk() {
+ val data = Register(userId = "abcdef", password = "abcdefghi")
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+ mockHostReferrerHash(hash)
+ mockPutSession()
+
+ every { dao.user("abcdef") } returns null
+
+ every { dao.createUser(any()) } just Runs
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.user?.userId == "abcdef"
+ })
+ }
+
+ }
+ }
+
+ @Test
+ fun testPostRegisterLoggedIn() {
+ val data = Register()
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningUser(dao)
+
+ every { request.headers[HttpHeaders.Host] } returns "host"
+
+ every {
+ application
+ .attributes
+ .get(ApplicationFeature.registry)
+ .get(Locations.key)
+ .href(any())
+ } returns "/redirect"
+
+ every {
+ response.headers.append(any(), any())
+ } just Runs
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ response.headers.append(HttpHeaders.Location, "http://host/redirect")
+ respond(HttpStatusCode.Found)
+ }
+ }
+ }
+
+
+ @Test
+ fun testPostRegisterPasswordSize() {
+ checkRegisterError(
+ Register(userId = "abcdefghi", password = "abcd"),
+ "Password should be at least 6 characters long")
+ }
+
+ @Test
+ fun testPostRegisterLoginSize() {
+ checkRegisterError(
+ Register(userId = "abc", password = "abcdefghi"),
+ "Login should be at least 4 characters long")
+ }
+
+ @Test
+ fun testPostRegisterLoginConsistsOfDigitsLetters() {
+ checkRegisterError(
+ Register(userId = "#@!$!#$", password = "abcdefghi"),
+ "Login should be consists of digits, letters, dots or underscores")
+ }
+
+ private fun checkRegisterError(data: Register, msg: String) {
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(LoginResponse(error = msg))
+ }
+ }
+ }
+
+ @Test
+ fun testPostRegisterSameUserRegistered() {
+ val data = Register(userId = "abcdef", password = "abcdefghi")
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+ mockHostReferrerHash(hash)
+
+ val user = User("abcdef", "abc@def", "Abc Def", "")
+ every { dao.user("abcdef") } returnsMany listOf(null, user)
+
+ every { dao.createUser(any()) } throws RuntimeException("failed to create user")
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(LoginResponse(error = "User with the following login is already registered"))
+ }
+
+ }
+ }
+
+ @Test
+ fun testPostRegisterSameEmailUserRegistered() {
+ val data = Register(userId = "abcdef", password = "abcdefghi", email = "user@email")
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+ mockHostReferrerHash(hash)
+
+ every { dao.user(any()) } returns null
+
+ every { dao.createUser(any()) } throws RuntimeException("failed to create user")
+
+ every { dao.userByEmail("user@email") } returns mockk()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(LoginResponse(error = "User with the following email user@email is already registered"))
+ }
+ }
+ }
+
+ @Test
+ fun testPostRegisterFailed() {
+ val data = Register(userId = "abcdef", password = "abcdefghi", email = "user@email")
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+ mockHostReferrerHash(hash)
+
+ every { dao.user(any()) } returns null
+
+ every { dao.createUser(any()) } throws RuntimeException("failed to create user")
+
+ every { dao.userByEmail("user@email") } returns null
+
+ every { route.application.environment.log.error(any()) } just Runs
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(LoginResponse(error = "Failed to register"))
+ }
+ }
+ }
+
+ @Test
+ fun testPostRegisterUserExists() {
+ val data = Register(userId = "abcdef", password = "abcdefghi")
+ postRegister.invokeBlock(locations, data) { handle ->
+ mockSessionReturningNothing()
+ mockUser(dao)
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(LoginResponse(error = "User with the following login is already registered"))
+ }
+
+ }
+ }
+
+ @Test
+ fun testGetRegisterNotAllowed() {
+ getRegister.invokeBlock(locations, Register()) { handle ->
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify { respond(HttpStatusCode.MethodNotAllowed) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/test/org/jetbrains/demo/thinkter/RouteMockDsl.kt b/backend/test/org/jetbrains/demo/thinkter/RouteMockDsl.kt
new file mode 100644
index 0000000..f2f597b
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/RouteMockDsl.kt
@@ -0,0 +1,82 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import kotlinx.coroutines.experimental.runBlocking
+import org.jetbrains.ktor.application.ApplicationCall
+import org.jetbrains.ktor.application.ApplicationFeature
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.pipeline.PipelineContext
+import org.jetbrains.ktor.pipeline.PipelineInterceptor
+import org.jetbrains.ktor.routing.Route
+import org.jetbrains.ktor.routing.RouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.jetbrains.ktor.routing.application
+import kotlin.reflect.KClass
+
+fun Route.mockDsl(locations: Locations, block: RouteDslMock.() -> Unit) = RouteDslMock(this, locations).block()
+
+typealias RouteBlockSlot = CapturingSlot>
+
+class RouteDslMock(val route: Route, val locations: Locations) {
+ init {
+ every {
+ route
+ .application
+ .attributes
+ .get(ApplicationFeature.registry)
+ .get(Locations.key)
+ } returns locations
+ }
+
+ inline fun RouteDslMock.mockObj(noinline block: RouteDslMock.() -> Unit) {
+ mockObj(this.route, T::class, block)
+ }
+
+ @PublishedApi
+ internal fun mockObj(route: Route, dataClass: KClass<*>, block: RouteDslMock.() -> Unit) {
+ val nextRoute = mockk()
+ every { locations.createEntry(route, dataClass) } returns nextRoute
+ every { nextRoute.parent } returns route
+
+ RouteDslMock(nextRoute, locations).block()
+ }
+
+ fun RouteDslMock.mockSelect(selector: RouteSelector, block: RouteDslMock.() -> Unit) {
+ val nextRoute = mockk()
+ every { route.select(selector) } returns nextRoute
+ every { nextRoute.parent } returns route
+
+ RouteDslMock(nextRoute, locations).block()
+ }
+
+ fun RouteDslMock.captureBlock(slot: RouteBlockSlot) {
+ every { route.handle(capture(slot)) } just Runs
+ }
+}
+
+typealias CallInvoker = () -> Unit
+
+fun RouteBlockSlot.invokeBlock(locations: Locations,
+ data: Any,
+ block: ApplicationCall.(CallInvoker) -> Unit) {
+
+ runBlocking {
+ val ctx = mockk>()
+
+ val call = mockk()
+
+ every {
+ val dataCls = data.javaClass.kotlin
+ locations.resolve(dataCls, call)
+ } returns data
+
+ every { ctx.subject } returns call
+
+ call.block {
+ runBlocking {
+ captured.invoke(ctx, call)
+ }
+ }
+ }
+
+}
diff --git a/backend/test/org/jetbrains/demo/thinkter/UserPageKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/UserPageKtTest.kt
new file mode 100644
index 0000000..c36b7f6
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/UserPageKtTest.kt
@@ -0,0 +1,71 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.UserThoughtsResponse
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.http.HttpStatusCode
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class UserPageKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getUserThoughts = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getUserThoughts)
+ }
+ }
+ }
+
+ route.userPage(dao)
+ }
+
+ @Test
+ fun testGetUserThoughtsOk() {
+ getUserThoughts.invokeBlock(locations, UserThoughts("abcdef")) { handle ->
+ mockHostReferrerHash(hash)
+ mockUser(dao)
+ mockGetThought(dao, 0)
+
+ every { dao.userThoughts("abcdef") } returns listOf(1, 2, 3)
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.thoughts.any { it.id == 2 && it.text == "text" }
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetUserThoughtsNoUserFound() {
+ getUserThoughts.invokeBlock(locations, UserThoughts("abcdef")) { handle ->
+ every { dao.user("abcdef") } returns null
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(HttpStatusCode.NotFound.description("User abcdef doesn't exist"))
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/backend/test/org/jetbrains/demo/thinkter/ViewThoughtKtTest.kt b/backend/test/org/jetbrains/demo/thinkter/ViewThoughtKtTest.kt
new file mode 100644
index 0000000..cefe093
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/ViewThoughtKtTest.kt
@@ -0,0 +1,77 @@
+package org.jetbrains.demo.thinkter
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.dao.ThinkterStorage
+import org.jetbrains.demo.thinkter.model.ViewThoughtResponse
+import org.jetbrains.ktor.http.HttpMethod
+import org.jetbrains.ktor.locations.Locations
+import org.jetbrains.ktor.routing.HttpMethodRouteSelector
+import org.jetbrains.ktor.routing.Routing
+import org.junit.Before
+import org.junit.Test
+
+class ViewThoughtKtTest {
+ val route = mockk()
+ val dao = mockk()
+ val hash = mockk<(String) -> String>()
+ val locations = mockk()
+
+ val getViewThought = RouteBlockSlot()
+ val postViewThought = RouteBlockSlot()
+
+ @Before
+ fun setUp() {
+ route.mockDsl(locations) {
+ mockObj {
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Get)) {
+ captureBlock(getViewThought)
+ }
+ mockSelect(HttpMethodRouteSelector(HttpMethod.Post)) {
+ captureBlock(postViewThought)
+ }
+ }
+ }
+
+ route.viewThought(dao, hash)
+ }
+
+ @Test
+ fun testGetPostThoughtOk() {
+ getViewThought.invokeBlock(locations, ViewThought(1)) { handle ->
+ mockSessionReturningUser(dao)
+ mockHostReferrerHash(hash)
+ mockGetThought(dao, 0)
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.thought.userId == "userId" &&
+ it.code!!.contains("cba:tsoh:dIresu")
+ })
+ }
+ }
+ }
+
+ @Test
+ fun testGetPostThoughtNotLoggedIn() {
+ getViewThought.invokeBlock(locations, ViewThought(1)) { handle ->
+ mockGetThought(dao, 0)
+ mockSessionReturningNothing()
+
+ coEvery { respond(any()) } just Runs
+
+ handle()
+
+ coVerify {
+ respond(assert {
+ it.thought.id == 1 &&
+ it.code == null
+ })
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/backend/test/org/jetbrains/demo/thinkter/dao/ThinkterDatabaseTest.kt b/backend/test/org/jetbrains/demo/thinkter/dao/ThinkterDatabaseTest.kt
new file mode 100644
index 0000000..7f7f89e
--- /dev/null
+++ b/backend/test/org/jetbrains/demo/thinkter/dao/ThinkterDatabaseTest.kt
@@ -0,0 +1,301 @@
+package org.jetbrains.demo.thinkter.dao
+
+import io.mockk.*
+import org.jetbrains.demo.thinkter.model.User
+import org.jetbrains.squash.connection.DatabaseConnection
+import org.jetbrains.squash.dialect.BaseSQLDialect
+import org.jetbrains.squash.expressions.alias
+import org.jetbrains.squash.expressions.invoke
+import org.jetbrains.squash.results.Response
+import org.jetbrains.squash.results.ResultRow
+import org.jetbrains.squash.results.get
+import org.jetbrains.squash.statements.DeleteQueryStatement
+import org.jetbrains.squash.statements.InsertValuesStatement
+import org.jetbrains.squash.statements.QueryStatement
+import org.junit.Assert
+import org.junit.Test
+import java.time.LocalDateTime
+
+class ThinkterDatabaseTest {
+ val connection = mockk("db")
+ fun tx() = connection.createTransaction()
+
+ init {
+ every { tx().databaseSchema().create(any()) } just Runs
+ every { tx().close() } just Runs
+ }
+
+ val database = ThinkterDatabase(connection)
+
+ @Test
+ fun countReplies() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+ every {
+ row.get(0)
+ } returns 3
+
+ val nReplies = database.countReplies(1)
+ Assert.assertEquals(3, nReplies)
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT COUNT(Thoughts.id), Thoughts.reply_to = ? FROM Thoughts" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun createThought() {
+ every {
+ with(tx()) {
+ any>().execute()
+ }
+ } returns 1
+
+ database.createThought("userId", "text")
+
+ verify {
+ with(tx()) {
+ assert> {
+ "INSERT INTO Thoughts (user_id, \"date\", reply_to, text) VALUES (?, ?, NULL, ?)" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun deleteThought() {
+ every {
+ with(tx()) {
+ any>().execute()
+ }
+ } just Runs
+
+ database.deleteThought(1)
+
+ verify {
+ with(tx()) {
+ assert> {
+ "DELETE FROM Thoughts WHERE Thoughts.id = ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun getThought() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+
+ every { row[Thoughts.user] } returns "user"
+ every { row[Thoughts.text] } returns "text"
+
+ every {
+ row[Thoughts.date]
+ } returns LocalDateTime.now()
+
+ every { row[Thoughts.replyTo] } returns null
+
+ database.getThought(1)
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT * FROM Thoughts WHERE Thoughts.id = ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun userThoughts() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+ every { row[Thoughts.id] } returns 1
+
+ database.userThoughts("id")
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT Thoughts.id FROM Thoughts WHERE Thoughts.user_id = ? ORDER BY Thoughts.\"date\" DESC NULLS LAST LIMIT ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun user() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+
+ every { row[Users.email] } returns "email"
+ every { row[Users.displayName] } returns "user"
+ every { row[Users.passwordHash] } returns "hash"
+
+ database.user("user", "hash")
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT * FROM Users WHERE Users.id = ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun userByEmail() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+
+ every { row[Users.id] } returns "user"
+ every { row[Users.email] } returns "email"
+ every { row[Users.displayName] } returns "user"
+ every { row[Users.passwordHash] } returns "hash"
+
+ database.userByEmail("email")
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT * FROM Users WHERE Users.email = ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun createUser() {
+ every {
+ with(tx()) {
+ any>().execute()
+ }
+ } just Runs
+
+ database.createUser(User("id", "email", "name", "pwd"))
+
+ verify {
+ with(tx()) {
+ assert> {
+ "INSERT INTO Users (id, display_name, email, password_hash) VALUES (?, ?, ?, ?)" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun top() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+ val k2 = Thoughts.alias("k2")
+ every { row[Thoughts.id(k2)] } returns 1
+
+ database.top()
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT Thoughts.id, COUNT(k2.id) FROM Thoughts LEFT OUTER JOIN Thoughts AS k2 ON Thoughts.id = k2.reply_to GROUP BY Thoughts.id ORDER BY COUNT(k2.id) DESC NULLS LAST LIMIT ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+ @Test
+ fun latest() {
+ val response = mockk()
+ val row = mockk()
+ every {
+ with(tx()) {
+ any().execute()
+ }
+ } returns response
+
+ mockSingleRowResponse(response, row)
+
+ every { row[Thoughts.id] } returns 1
+
+ database.latest(1)
+
+ verify {
+ with(tx()) {
+ assert {
+ "SELECT Thoughts.id FROM Thoughts WHERE Thoughts.\"date\" > ? ORDER BY Thoughts.\"date\" DESC NULLS LAST LIMIT ?" ==
+ BaseSQLDialect("dialect").statementSQL(it).sql
+ }.execute()
+ }
+ }
+ }
+
+
+ private fun mockSingleRowResponse(response: Response, row: ResultRow) {
+ every {
+ response.iterator().hasNext()
+ } returnsMany listOf(true, false)
+
+ every {
+ response.iterator().next()
+ } returnsMany listOf(row)
+ }
+}
diff --git a/backend/testResources/logback-test.xml b/backend/testResources/logback-test.xml
new file mode 100644
index 0000000..ab4454b
--- /dev/null
+++ b/backend/testResources/logback-test.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 74bb778..5281128 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Wed Nov 08 07:41:52 CET 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-all.zip