import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class CommentService(
private val commentRepository: CommentRepository,
private val userService: UserService,
private val commentModelFactory: CommentModelFactory,
private val commentValidator: CommentValidator,
private val emailService: EmailService,
private val backgroundScope: CoroutineScope,
) {
suspend fun addComment(
token: String,
collectionKey: String,
body: AddComment
) {
val userId = userService.readUserId(token)
if (!commentValidator.validate(body.comment)) {
throw CommentValidationException("Invalid comment")
}
val commentModel = commentModelFactory
.toCommentModel(userId, collectionKey, body)
commentRepository.addComment(commentModel)
notifyAddCommentObservers(collectionKey, body)
}
suspend fun getComments(
collectionKey: String
) = coroutineScope {
val commentDocuments = commentRepository
.getComments(collectionKey)
val users: Map<String, User> = commentDocuments
.map { it.userId }
.toSet()
.map { async { userService.findUserById(it) } }
.awaitAll()
.associateBy { it.id }
CommentsCollection(
collectionKey = collectionKey,
elements = commentDocuments.map { commentModel ->
CommentElement(
id = commentModel.id,
collectionKey = commentModel.collectionKey,
user = users[commentModel.userId],
comment = commentModel.comment,
date = commentModel.date,
)
}
)
}
// For legacy blocking calls
fun addCommentBlocking(
token: String,
collectionKey: String,
body: AddComment
) = runBlocking {
addComment(token, collectionKey, body)
}
// For legacy blocking calls
fun getCommentsBlocking(
collectionKey: String
): CommentsCollection = runBlocking {
getComments(collectionKey)
}
private fun notifyAddCommentObservers(collectionKey: String, body: AddComment) {
backgroundScope.launch {
val observerIds = commentRepository.getCollectionKeyObservers(collectionKey)
val users = observerIds.map { userService.findUserById(it) }
users.forEach { user ->
launch {
emailService.notifyAboutCommentInObservedCollection(
email = user.email,
collectionKey = collectionKey,
comment = body.comment
)
}
}
}
}
}
interface CommentRepository {
suspend fun getComments(collectionKey: String): List<CommentModel>
suspend fun getComment(id: String): CommentModel?
suspend fun addComment(comment: CommentModel)
suspend fun deleteComment(commentId: String)
suspend fun getCollectionKeyObservers(collectionKey: String): List<String>
}
class CommentModelFactory(
private val uuidProvider: UuidProvider,
private val timeProvider: TimeProvider,
) {
fun toCommentModel(userId: String, collectionKey: String, body: AddComment) = CommentModel(
id = uuidProvider.next(),
collectionKey = collectionKey,
userId = userId,
comment = body.comment,
date = timeProvider.now()
)
}
interface EmailService {
suspend fun notifyAboutCommentInObservedCollection(email: String, collectionKey: String, comment: String?)
}
interface TimeProvider {
fun now(): Instant
}
interface UuidProvider {
fun next(): String
}
interface CommentValidator {
@Throws(CommentValidationException::class)
fun validate(comment: String?): Boolean
}
class CommentValidationException(message: String) : Exception(message)
data class CommentModel(
val id: String,
val collectionKey: String,
val userId: String,
val comment: String?,
val date: Instant,
)
interface UserService {
fun readUserId(token: String): String
suspend fun findUser(token: String): User
suspend fun findUserById(id: String): User
}
object NoSuchUserException: Exception("No such user")
data class CommentsCollection(
val collectionKey: String,
val elements: List<CommentElement>,
)
data class CommentElement(
val id: String,
val collectionKey: String,
val user: User?,
val comment: String?,
val date: Instant,
)
data class AddComment(
val comment: String?,
)
data class EditComment(
val comment: String?,
)
data class User(
val id: String,
val email: String,
val imageUrl: String,
val displayName: String? = null,
val bio: String? = null,
)
data class UserDocument(
val _id: String,
val email: String,
val imageUrl: String,
val displayName: String? = null,
val bio: String? = null,
)
fun UserDocument.toUser() = User(
id = _id,
email = email,
imageUrl = imageUrl,
displayName = displayName,
bio = bio
)
fun User.toUserDocument() = UserDocument(
_id = id,
email = email,
imageUrl = imageUrl,
displayName = displayName,
bio = bio
)
class CommentServiceTests {
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val commentsRepository = FakeCommentRepository()
private val userService = FakeUserService()
private val uuidProvider = FakeUuidProvider()
private val timeProvider = FakeTimeProvider()
private val commentValidator = FakeCommentValidator()
private val emailService = FakeEmailService()
private val commentsFactory: CommentModelFactory = CommentModelFactory(uuidProvider, timeProvider)
private val commentService: CommentService = CommentService(
commentsRepository,
userService,
commentsFactory,
commentValidator,
emailService,
testScope
)
@Before
fun setup() {
Initialize common test data
userService.hasUsers(user1, user2)
commentValidator.setShouldValidate(true)
}
@After
fun cleanup() {
timeProvider.clean()
uuidProvider.clean()
commentsRepository.clean()
userService.clear()
commentValidator.reset()
emailService.clear()
}
@Test
fun `Should add comment`() = runTest {
// given
commentsRepository.has(
commentModel1,
)
userService.hasUsers(
user1,
user2
)
userService.hasToken(aToken, user2.id)
uuidProvider.alwaysReturn(commentModel2.id)
timeProvider.advanceTimeTo(commentModel2.date)
// when
commentService.addComment(aToken, collectionKey2, AddComment(commentModel2.comment))
// then
assertEquals(commentModel2, commentsRepository.getComment(commentModel2.id))
}
@Test
fun `Should get comments by collection key`() = runTest {
// given
commentsRepository.has(
commentModel1,
commentModel2,
commentDocument3
)
userService.hasUsers(
user1,
user2
)
// when
val result: CommentsCollection = commentService.getComments(collectionKey1)
// then
with(result) {
assertEquals(collectionKey1, collectionKey)
assertEquals(listOf(commentElement1, commentElement3), elements)
}
}
@Test
fun `Should concurrently find users when getting comments`() = runTest {
// given
commentsRepository.has(
commentModel1,
commentModel1,
commentModel1,
commentModel2,
commentDocument3,
)
userService.hasUsers(
user1,
user2
)
userService.findUserDelay = 1000
// when
commentService.getComments(collectionKey1)
// then
assertEquals(1000, currentTime)
}
@Test
fun `Should add comment using blocking method`() {
// given
commentsRepository.has(
commentModel1,
)
userService.hasUsers(
user1,
user2
)
userService.hasToken(aToken, user2.id)
uuidProvider.alwaysReturn(commentModel2.id)
timeProvider.advanceTimeTo(commentModel2.date)
// when
commentService.addCommentBlocking(aToken, collectionKey2, AddComment(commentModel2.comment))
// then
runBlocking {
assertEquals(commentModel2, commentsRepository.getComment(commentModel2.id))
}
}
@Test
fun `Should get comments by collection key using blocking method`() {
// given
commentsRepository.has(
commentModel1,
commentModel2,
commentDocument3
)
userService.hasUsers(
user1,
user2
)
// when
val result: CommentsCollection = commentService.getCommentsBlocking(collectionKey1)
// then
with(result) {
assertEquals(collectionKey1, collectionKey)
assertEquals(listOf(commentElement1, commentElement3), elements)
}
}
Fake Data
private val aToken = "SOME_TOKEN"
private val collectionKey1 = "SOME_COLLECTION_KEY_1"
private val collectionKey2 = "SOME_COLLECTION_KEY_2"
private val date1 = Instant.parse("2018-11-30T18:35:24.00Z")
private val date2 = Instant.parse("2019-11-30T18:35:24.00Z")
private val userDocument1 = UserDocument(
_id = "U_ID_1",
email = "user1@email.com",
imageUrl = "some_image_1",
displayName = "some_display_name_1",
bio = "some bio 1"
)
private val userDocument2 = UserDocument(
_id = "U_ID_2",
email = "user2@email.com",
imageUrl = "some_image_2",
displayName = "some_display_name_2",
bio = "some bio 2"
)
private val user1 = userDocument1.toUser()
private val user2 = userDocument2.toUser()
private val commentModel1 = CommentModel(
id = "C_ID_1",
collectionKey = collectionKey1,
userId = user1.id,
comment = "Some comment 1",
date = date1,
)
private val commentModel2 = CommentModel(
id = "C_ID_2",
collectionKey = collectionKey2,
userId = user2.id,
comment = "Some comment 2",
date = date2,
)
private val commentDocument3 = CommentModel(
id = "C_ID_3",
collectionKey = collectionKey1,
userId = user2.id,
comment = "Some comment 3",
date = date2,
)
private val commentElement1 = CommentElement(
id = "C_ID_1",
collectionKey = collectionKey1,
user = user1,
comment = "Some comment 1",
date = date1,
)
private val commentElement2 = CommentElement(
id = "C_ID_2",
collectionKey = collectionKey2,
user = user2,
comment = "Some comment 2",
date = date2,
)
private val commentElement3 = CommentElement(
id = "C_ID_3",
collectionKey = collectionKey1,
user = user2,
comment = "Some comment 3",
date = date2,
)
@Test
fun `Should add comment when validation passes`() = runTest {
// given
commentValidator.setShouldValidate(true)
userService.hasToken(aToken, user2.id)
uuidProvider.alwaysReturn(commentModel2.id)
timeProvider.advanceTimeTo(commentModel2.date)
// when
commentService.addComment(aToken, collectionKey2, AddComment(commentModel2.comment))
// then
assertEquals(commentModel2, commentsRepository.getComment(commentModel2.id))
}
@Test
fun `Should throw exception when validation fails`() = runTest {
// given
commentValidator.setShouldValidate(false)
userService.hasToken(aToken, user2.id)
// when/then
assertFailsWith<CommentValidationException> {
commentService.addComment(aToken, collectionKey2, AddComment(commentModel2.comment))
}
}
@Test
fun `Should throw exception when validation fails with custom predicate`() = runTest {
// given
commentValidator.setValidationPredicate { comment ->
comment != null && comment.length >= 5
}
userService.hasToken(aToken, user2.id)
// when/then
assertFailsWith<CommentValidationException> {
commentService.addComment(aToken, collectionKey2, AddComment("abc"))
}
}
@Test
fun `Should send emails to observers when comment is added`() = runTest(testDispatcher) {
// given
userService.hasUsers(user1, user2)
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
Set up observers for the collection
val observers = listOf(user2.id)
commentsRepository.setObservers(collectionKey1, observers)
// when
commentService.addComment(aToken, collectionKey1, AddComment(commentModel1.comment))
Use the testDispatcher to run all pending tasks
runCurrent()
advanceUntilIdle()
// then
val expectedEmails = listOf(
user2.email to "New comment in collection $collectionKey1: ${commentModel1.comment}"
)
assertEquals(expectedEmails, emailService.getEmailsSent())
}
@Test
fun `Should send emails to multiple observers when comment is added`() = runTest(testDispatcher) {
// given
val user3 = User(
id = "U_ID_3",
email = "user3@email.com",
imageUrl = "some_image_3",
displayName = "some_display_name_3",
bio = "some bio 3"
)
userService.hasUsers(user1, user2, user3)
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
Set up multiple observers for the collection
val observers = listOf(user2.id, user3.id)
commentsRepository.setObservers(collectionKey1, observers)
// when
commentService.addComment(aToken, collectionKey1, AddComment(commentModel1.comment))
Use the testDispatcher to run all pending tasks
runCurrent()
advanceUntilIdle()
// then
val expectedEmails = listOf(
user2.email to "New comment in collection $collectionKey1: ${commentModel1.comment}",
user3.email to "New comment in collection $collectionKey1: ${commentModel1.comment}"
)
assertEquals(expectedEmails, emailService.getEmailsSent())
}
@Test
fun `Should throw exception when token is invalid`() = runTest {
// given
val invalidToken = "INVALID_TOKEN"
Not registering this token with hasToken, so it will be invalid
// when/then
assertFailsWith<NoSuchUserException> {
commentService.addComment(invalidToken, collectionKey1, AddComment("Some comment"))
}
}
@Test
fun `Should throw exception when user ID does not exist`() = runTest {
// given
val nonExistentUserId = "NON_EXISTENT_USER_ID"
Add a comment with a non-existent user ID
val commentWithNonExistentUser = CommentModel(
id = "C_ID_NON_EXISTENT",
collectionKey = collectionKey1,
userId = nonExistentUserId,
comment = "Comment from non-existent user",
date = date1
)
commentsRepository.has(commentWithNonExistentUser)
// when/then
assertFailsWith<NoSuchUserException> {
commentService.getComments(collectionKey1) // This will try to find the non-existent user
}
}
@Test
fun `Should return empty list when getting comments from empty collection`() = runTest {
// given
val emptyCollectionKey = "EMPTY_COLLECTION_KEY"
// when
val result = commentService.getComments(emptyCollectionKey)
// then
assertEquals(emptyCollectionKey, result.collectionKey)
assertEquals(emptyList(), result.elements)
}
@Test
fun `Should not send emails when there are no observers`() = runTest {
// given
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
No observers set up for the collection
commentsRepository.setObservers(collectionKey1, emptyList())
// when
commentService.addComment(aToken, collectionKey1, AddComment(commentModel1.comment))
Use the testDispatcher to run all pending tasks
testDispatcher.scheduler.runCurrent()
advanceUntilIdle()
// then
assertEquals(emptyList(), emailService.getEmailsSent())
}
@Test
fun `Should handle null comment when validator allows it`() = runTest {
// given
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
Default validator allows null comments
// when
commentService.addComment(aToken, collectionKey1, AddComment(null))
// then
val comment = commentsRepository.getComment(commentModel1.id)
assertEquals(null, comment?.comment)
}
@Test
fun `Should throw exception when null comment is rejected by validator`() = runTest {
// given
userService.hasToken(aToken, user1.id)
Configure validator to reject null comments
commentValidator.setValidationPredicate { comment -> comment != null }
// when/then
assertFailsWith<CommentValidationException> {
commentService.addComment(aToken, collectionKey1, AddComment(null))
}
}
@Test
fun `Should improve concurrent test with better assertions`() = runTest {
// given
Create three different comments with the same user ID but different comment IDs
val comment1 = CommentModel(
id = "C_ID_CONCURRENT_1",
collectionKey = collectionKey1,
userId = user1.id,
comment = "Concurrent comment 1",
date = date1
)
val comment2 = CommentModel(
id = "C_ID_CONCURRENT_2",
collectionKey = collectionKey1,
userId = user1.id,
comment = "Concurrent comment 2",
date = date1
)
val comment3 = CommentModel(
id = "C_ID_CONCURRENT_3",
collectionKey = collectionKey1,
userId = user1.id,
comment = "Concurrent comment 3",
date = date1
)
commentsRepository.has(comment1, comment2, comment3)
userService.findUserDelay = 1000
// when
val startTime = currentTime
val result = commentService.getComments(collectionKey1)
val endTime = currentTime
// then
Verify that the operation took exactly the time of one user lookup, not three
assertEquals(1000, endTime - startTime)
Verify the result contains the expected number of comments
assertEquals(collectionKey1, result.collectionKey)
assertEquals(3, result.elements.size)
Verify all comments have the same user
result.elements.forEach { element ->
assertEquals(user1, element.user)
}
Verify the comments have the expected IDs
val commentIds = result.elements.map { it.id }.toSet()
assertEquals(setOf("C_ID_CONCURRENT_1", "C_ID_CONCURRENT_2", "C_ID_CONCURRENT_3"), commentIds)
}
@Test
fun `Should call each notification in a separate coroutine`() = runTest(testDispatcher) {
// given
val user3 = User(
id = "U_ID_3",
email = "user3@email.com",
imageUrl = "some_image_3",
displayName = "some_display_name_3",
bio = "some bio 3"
)
val user4 = User(
id = "U_ID_4",
email = "user4@email.com",
imageUrl = "some_image_4",
displayName = "some_display_name_4",
bio = "some bio 4"
)
userService.hasUsers(user1, user2, user3, user4)
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
Set up multiple observers for the collection
val observers = listOf(user2.id, user3.id, user4.id)
commentsRepository.setObservers(collectionKey1, observers)
Set a delay for notifications to ensure we can detect concurrency
emailService.notificationDelay = 1000
// when
commentService.addComment(aToken, collectionKey1, AddComment(commentModel1.comment))
Use the testDispatcher to run all pending tasks
runCurrent()
advanceUntilIdle()
// then
Verify all emails were sent
val expectedEmails = listOf(
user2.email to "New comment in collection $collectionKey1: ${commentModel1.comment}",
user3.email to "New comment in collection $collectionKey1: ${commentModel1.comment}",
user4.email to "New comment in collection $collectionKey1: ${commentModel1.comment}"
)
assertEquals(expectedEmails, emailService.getEmailsSent())
Verify that notifications were called concurrently
If they were called sequentially, maxConcurrentNotifications would be 1
If they were called concurrently, maxConcurrentNotifications should be equal to the number of observers
assertEquals(3, emailService.getMaxConcurrentNotifications())
}
@Test
fun `Should not wait for notification process to complete`() = runTest(testDispatcher) {
// given
userService.hasUsers(user1, user2)
userService.hasToken(aToken, user1.id)
uuidProvider.alwaysReturn(commentModel1.id)
timeProvider.advanceTimeTo(commentModel1.date)
Set up observers for the collection
val observers = listOf(user2.id)
commentsRepository.setObservers(collectionKey1, observers)
Set a long delay for notifications to ensure we can detect if we're waiting
emailService.notificationDelay = 10000
// when
val startTime = currentTime
commentService.addComment(aToken, collectionKey1, AddComment(commentModel1.comment))
val endTime = currentTime
// then
Verify that addComment returns immediately, without waiting for the notification process to complete
assertEquals(0, endTime - startTime)
Verify that the notification process is started but not completed
runCurrent() // Run the initial part of the background task
assertTrue(emailService.isNotificationStarted())
assertFalse(emailService.isNotificationCompleted())
assertEquals(emptyList(), emailService.getEmailsSent())
Advance time to complete the notification process
advanceUntilIdle()
Verify that the notification process is completed and emails are sent
assertTrue(emailService.isNotificationCompleted())
val expectedEmails = listOf(
user2.email to "New comment in collection $collectionKey1: ${commentModel1.comment}"
)
assertEquals(expectedEmails, emailService.getEmailsSent())
}
@Test
fun `Should not fetch the same user more than once when getting comments`() = runTest {
// given
Create multiple comments with the same user IDs to test deduplication
val comment1 = CommentModel(
id = "C_ID_DEDUP_1",
collectionKey = collectionKey1,
userId = user1.id,
comment = "Comment 1 from user1",
date = date1
)
val comment2 = CommentModel(
id = "C_ID_DEDUP_2",
collectionKey = collectionKey1,
userId = user1.id, // Same user as comment1
comment = "Comment 2 from user1",
date = date1
)
val comment3 = CommentModel(
id = "C_ID_DEDUP_3",
collectionKey = collectionKey1,
userId = user2.id,
comment = "Comment from user2",
date = date1
)
val comment4 = CommentModel(
id = "C_ID_DEDUP_4",
collectionKey = collectionKey1,
userId = user2.id, // Same user as comment3
comment = "Another comment from user2",
date = date1
)
commentsRepository.has(comment1, comment2, comment3, comment4)
userService.hasUsers(user1, user2)
// when
val result = commentService.getComments(collectionKey1)
// then
Verify we got all 4 comments
assertEquals(4, result.elements.size)
Verify that findUserById was called exactly once for each unique user ID
assertEquals(1, userService.getFindUserByIdCalls(user1.id))
assertEquals(1, userService.getFindUserByIdCalls(user2.id))
Verify the total number of calls matches the number of unique users
val allCalls = userService.getAllFindUserByIdCalls()
assertEquals(2, allCalls.size)
assertEquals(2, allCalls.values.sum())
}
class FakeTimeProvider : TimeProvider {
private var currentTime = DEFAULT_START
override fun now(): Instant = currentTime
fun advanceTimeTo(instant: Instant) {
currentTime = instant
}
fun advanceTimeByDays(days: Int) {
currentTime = currentTime.plusSeconds(1L * days * 60 * 60 * 24)
}
fun clean() {
currentTime = DEFAULT_START
}
fun advanceTime() {
currentTime = currentTime.plusSeconds(10)
}
companion object {
val DEFAULT_START = Instant.parse("2018-11-30T18:35:24.00Z")
}
}
class FakeCommentRepository: CommentRepository {
private var comments = listOf<CommentModel>()
private var collectionObservers = mapOf<String, List<String>>()
fun has(vararg comment: CommentModel) {
comments = comments + comment
}
fun clean() {
comments = emptyList()
collectionObservers = emptyMap()
}
fun setObservers(collectionKey: String, observers: List<String>) {
collectionObservers = collectionObservers + (collectionKey to observers)
}
override suspend fun getComments(collectionKey: String): List<CommentModel> =
comments.filter { it.collectionKey == collectionKey }
override suspend fun getComment(id: String): CommentModel? =
comments.find { it.id == id }
override suspend fun addComment(comment: CommentModel) {
comments = comments + comment
}
override suspend fun deleteComment(commentId: String) {
TODO("Not yet implemented")
}
override suspend fun getCollectionKeyObservers(collectionKey: String): List<String> =
collectionObservers[collectionKey] ?: emptyList()
}
class FakeCommentValidator : CommentValidator {
private var shouldValidate = true
private var validationPredicate: (String?) -> Boolean = { true }
fun setShouldValidate(shouldValidate: Boolean) {
this.shouldValidate = shouldValidate
}
fun setValidationPredicate(predicate: (String?) -> Boolean) {
this.validationPredicate = predicate
}
fun reset() {
shouldValidate = true
validationPredicate = { true }
}
override fun validate(comment: String?): Boolean {
return shouldValidate && validationPredicate(comment)
}
}
class FakeEmailService : EmailService {
private var emailsSent = mutableListOf<Pair<String, String>>()
var notificationDelay: Long = 1000 // Default delay of 1 second
private val concurrentNotifications = AtomicInteger(0)
private var maxConcurrentNotifications = 0
private val notificationStarted = AtomicBoolean(false)
private val notificationCompleted = AtomicBoolean(false)
override suspend fun notifyAboutCommentInObservedCollection(email: String, collectionKey: String, comment: String?) {
notificationStarted.set(true)
val currentConcurrent = concurrentNotifications.incrementAndGet()
Update max concurrent notifications
synchronized(this) {
if (currentConcurrent > maxConcurrentNotifications) {
maxConcurrentNotifications = currentConcurrent
}
}
Simulate work with delay
delay(notificationDelay)
val body = "New comment in collection $collectionKey: $comment"
Add the email to the list of sent emails
synchronized(this) {
emailsSent.add(email to body)
}
concurrentNotifications.decrementAndGet()
notificationCompleted.set(true)
}
fun getEmailsSent(): List<Pair<String, String>> = emailsSent.toList()
fun getMaxConcurrentNotifications(): Int = maxConcurrentNotifications
fun isNotificationStarted(): Boolean = notificationStarted.get()
fun isNotificationCompleted(): Boolean = notificationCompleted.get()
fun clear() {
synchronized(this) {
emailsSent.clear()
}
maxConcurrentNotifications = 0
concurrentNotifications.set(0)
notificationStarted.set(false)
notificationCompleted.set(false)
}
}
class FakeUserService : UserService {
var findUserDelay: Long? = null
private var users = listOf<User>()
private var tokens = mapOf<String, String>()
private val findUserByIdCalls = mutableMapOf<String, Int>()
fun hasUsers(vararg user: User) {
users = users + user
}
fun hasToken(token: String, userId: String) {
tokens = tokens + (token to userId)
}
fun clear() {
users = emptyList()
tokens = mapOf()
findUserDelay = null
findUserByIdCalls.clear()
}
fun getFindUserByIdCalls(id: String): Int {
return findUserByIdCalls[id] ?: 0
}
fun getAllFindUserByIdCalls(): Map<String, Int> {
return findUserByIdCalls.toMap()
}
override fun readUserId(token: String): String =
tokens[token] ?: throw NoSuchUserException
override suspend fun findUser(token: String): User {
findUserDelay?.let { delay(it) }
return findUserById(readUserId(token))
}
override suspend fun findUserById(id: String): User {
findUserDelay?.let { delay(it) }
findUserByIdCalls[id] = (findUserByIdCalls[id] ?: 0) + 1
return users.find { it.id == id } ?: throw NoSuchUserException
}
}
class FakeUuidProvider: UuidProvider {
private var counter = 1
private var constantReturn: String? = null
override fun next(): String = constantReturn ?: "UUID#" + (counter++)
fun clean() {
counter = 1
constantReturn = null
}
fun alwaysReturn(value: String) {
constantReturn = value
}
}
}