Skip to content

Commit c3bfaa5

Browse files
committed
Part2: Added persistence and integration tests
1 parent 5b846f0 commit c3bfaa5

File tree

7 files changed

+224
-4
lines changed

7 files changed

+224
-4
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ dependencies {
3131
testImplementation("org.springframework.boot:spring-boot-starter-test") {
3232
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
3333
}
34+
35+
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
36+
runtimeOnly("com.h2database:h2")
3437
}
3538

3639
tasks.withType<Test> {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.kotlin.chat.repository
2+
3+
import org.springframework.data.annotation.Id
4+
import org.springframework.data.relational.core.mapping.Table
5+
import java.time.Instant
6+
7+
@Table("MESSAGES")
8+
data class Message(
9+
val content: String,
10+
val contentType: ContentType,
11+
val sent: Instant,
12+
val username: String,
13+
val userAvatarImageLink: String,
14+
@Id var id: String? = null)
15+
16+
enum class ContentType {
17+
PLAIN
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.example.kotlin.chat.repository
2+
3+
import org.springframework.data.jdbc.repository.query.Query
4+
import org.springframework.data.repository.CrudRepository
5+
import org.springframework.data.repository.query.Param
6+
7+
interface MessageRepository : CrudRepository<Message, String> {
8+
9+
// language=SQL
10+
@Query("""
11+
SELECT * FROM (
12+
SELECT * FROM MESSAGES
13+
ORDER BY "SENT" DESC
14+
LIMIT 10
15+
) ORDER BY "SENT"
16+
""")
17+
fun findLatest(): List<Message>
18+
19+
// language=SQL
20+
@Query("""
21+
SELECT * FROM (
22+
SELECT * FROM MESSAGES
23+
WHERE SENT > (SELECT SENT FROM MESSAGES WHERE ID = :id)
24+
ORDER BY "SENT" DESC
25+
) ORDER BY "SENT"
26+
""")
27+
fun findLatest(@Param("id") id: String): List<Message>
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.example.kotlin.chat.service
2+
3+
import com.example.kotlin.chat.repository.ContentType
4+
import com.example.kotlin.chat.repository.Message
5+
import com.example.kotlin.chat.repository.MessageRepository
6+
import org.springframework.context.annotation.Primary
7+
import org.springframework.stereotype.Service
8+
import java.net.URL
9+
10+
@Service
11+
@Primary
12+
class PersistentMessageService(val messageRepository: MessageRepository) : MessageService {
13+
14+
override fun latest(): List<MessageVM> =
15+
messageRepository.findLatest()
16+
.map { with(it) { MessageVM(content, UserVM(username, URL(userAvatarImageLink)), sent, id) } }
17+
18+
override fun after(messageId: String): List<MessageVM> =
19+
messageRepository.findLatest(messageId)
20+
.map { with(it) { MessageVM(content, UserVM(username, URL(userAvatarImageLink)), sent, id) } }
21+
22+
override fun post(message: MessageVM) {
23+
messageRepository.save(
24+
with(message) { Message(content, ContentType.PLAIN, sent, user.name, user.avatarImageLink.toString()) }
25+
)
26+
}
27+
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
1+
spring.datasource.schema=classpath:sql/schema.sql
2+
spring.datasource.url=jdbc:h2:file:./build/data/testdb
3+
spring.datasource.driverClassName=org.h2.Driver
4+
spring.datasource.username=sa
5+
spring.datasource.password=password
6+
spring.batch.initialize-schema=always

src/main/resources/sql/schema.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CREATE TABLE IF NOT EXISTS messages
2+
(
3+
id VARCHAR(60) DEFAULT RANDOM_UUID() PRIMARY KEY,
4+
content VARCHAR NOT NULL,
5+
content_type VARCHAR(128) NOT NULL,
6+
sent TIMESTAMP NOT NULL,
7+
username VARCHAR(60) NOT NULL,
8+
user_avatar_image_link VARCHAR(256) NOT NULL
9+
);
Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,143 @@
11
package com.example.kotlin.chat
22

3+
import com.example.kotlin.chat.repository.ContentType
4+
import com.example.kotlin.chat.repository.Message
5+
import com.example.kotlin.chat.repository.MessageRepository
6+
import com.example.kotlin.chat.service.MessageVM
7+
import com.example.kotlin.chat.service.UserVM
8+
import org.assertj.core.api.Assertions.assertThat
9+
import org.junit.jupiter.api.AfterEach
10+
import org.junit.jupiter.api.BeforeEach
311
import org.junit.jupiter.api.Test
12+
import org.junit.jupiter.params.ParameterizedTest
13+
import org.junit.jupiter.params.provider.ValueSource
14+
import org.springframework.beans.factory.annotation.Autowired
415
import org.springframework.boot.test.context.SpringBootTest
16+
import org.springframework.boot.test.web.client.TestRestTemplate
17+
import org.springframework.boot.test.web.client.postForEntity
18+
import org.springframework.core.ParameterizedTypeReference
19+
import org.springframework.http.HttpMethod
20+
import org.springframework.http.RequestEntity
21+
import java.net.URI
22+
import java.net.URL
23+
import java.time.Instant
24+
import java.time.temporal.ChronoUnit.MILLIS
525

6-
@SpringBootTest
26+
@SpringBootTest(
27+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
28+
properties = [
29+
"spring.datasource.url=jdbc:h2:mem:testdb"
30+
]
31+
)
732
class ChatKotlinApplicationTests {
833

34+
@Autowired
35+
lateinit var client: TestRestTemplate
36+
37+
@Autowired
38+
lateinit var messageRepository: MessageRepository
39+
40+
lateinit var lastMessageId: String
41+
42+
val now: Instant = Instant.now()
43+
44+
@BeforeEach
45+
fun setUp() {
46+
val secondBeforeNow = now.minusSeconds(1)
47+
val twoSecondBeforeNow = now.minusSeconds(2)
48+
val savedMessages = messageRepository.saveAll(
49+
listOf(
50+
Message(
51+
"*testMessage*",
52+
ContentType.PLAIN,
53+
twoSecondBeforeNow,
54+
"test",
55+
"http://test.com"
56+
),
57+
Message(
58+
"**testMessage2**",
59+
ContentType.PLAIN,
60+
secondBeforeNow,
61+
"test1",
62+
"http://test.com"
63+
),
64+
Message(
65+
"`testMessage3`",
66+
ContentType.PLAIN,
67+
now,
68+
"test2",
69+
"http://test.com"
70+
)
71+
)
72+
)
73+
lastMessageId = savedMessages.first().id ?: ""
74+
}
75+
76+
@AfterEach
77+
fun tearDown() {
78+
messageRepository.deleteAll()
79+
}
80+
81+
@ParameterizedTest
82+
@ValueSource(booleans = [true, false])
83+
fun `test that messages API returns latest messages`(withLastMessageId: Boolean) {
84+
val messages: List<MessageVM>? = client.exchange(
85+
RequestEntity<Any>(
86+
HttpMethod.GET,
87+
URI("/api/v1/messages?lastMessageId=${if (withLastMessageId) lastMessageId else ""}")
88+
),
89+
object : ParameterizedTypeReference<List<MessageVM>>() {}).body
90+
91+
if (!withLastMessageId) {
92+
assertThat(messages?.map { with(it) { copy(id = null, sent = sent.truncatedTo(MILLIS)) } })
93+
.first()
94+
.isEqualTo(
95+
MessageVM(
96+
"*testMessage*",
97+
UserVM("test", URL("http://test.com")),
98+
now.minusSeconds(2).truncatedTo(MILLIS)
99+
)
100+
)
101+
}
102+
103+
assertThat(messages?.map { with(it) { copy(id = null, sent = sent.truncatedTo(MILLIS)) } })
104+
.containsSubsequence(
105+
MessageVM(
106+
"**testMessage2**",
107+
UserVM("test1", URL("http://test.com")),
108+
now.minusSeconds(1).truncatedTo(MILLIS)
109+
),
110+
MessageVM(
111+
"`testMessage3`",
112+
UserVM("test2", URL("http://test.com")),
113+
now.truncatedTo(MILLIS)
114+
)
115+
)
116+
}
117+
118+
9119
@Test
10-
fun contextLoads() {
11-
}
120+
fun `test that messages posted to the API is stored`() {
121+
client.postForEntity<Any>(
122+
URI("/api/v1/messages"),
123+
MessageVM(
124+
"`HelloWorld`",
125+
UserVM("test", URL("http://test.com")),
126+
now.plusSeconds(1)
127+
)
128+
)
12129

130+
messageRepository.findAll()
131+
.first { it.content.contains("HelloWorld") }
132+
.apply {
133+
assertThat(this.copy(id = null, sent = sent.truncatedTo(MILLIS)))
134+
.isEqualTo(Message(
135+
"`HelloWorld`",
136+
ContentType.PLAIN,
137+
now.plusSeconds(1).truncatedTo(MILLIS),
138+
"test",
139+
"http://test.com"
140+
))
141+
}
142+
}
13143
}

0 commit comments

Comments
 (0)