Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ data class FeedbackChannelDTO(
val title: String,
val speakers: List<String>,
val channelId: String,
val ratingCategories: List<FeedbackChannelRatingCategoryDTO>
val ratingCategories: List<FeedbackChannelRatingCategoryDTO>,
val isOpen: Boolean
)

fun FeedbackChannel.toDTO(): FeedbackChannelDTO {
Expand All @@ -21,6 +22,7 @@ fun FeedbackChannel.toDTO(): FeedbackChannelDTO {
title = it.name,
id = it.id,
)
}
},
isOpen = isOpen
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package no.javazone.feedback.request.channel

import kotlinx.serialization.Serializable

@Serializable
data class FeedbackChannelUpdateDTO(
val isOpen: Boolean? = null
)
33 changes: 33 additions & 0 deletions core/src/main/kotlin/no/javazone/feedback/setupRouting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondOutputStream
import io.ktor.server.routing.get
import io.ktor.server.routing.patch
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import no.javazone.feedback.database.isDatabaseHealthy
import no.javazone.feedback.database.repository.FeedbackRepositoryDb
import no.javazone.feedback.domain.adapters.FeedbackAdapter
import no.javazone.feedback.domain.FeedbackChannel
import no.javazone.feedback.domain.errors.ChannelClosedError
import no.javazone.feedback.domain.errors.ChannelNotFoundError
import no.javazone.feedback.domain.generators.ExternalIdGeneratorDefault
import no.javazone.feedback.pages.feedbackPage
Expand All @@ -23,6 +26,7 @@ import no.javazone.feedback.pages.thankYouFragment
import no.javazone.feedback.qrcode.QRCodeGenerator
import no.javazone.feedback.request.channel.FeedbackChannelCreationDTO
import no.javazone.feedback.request.channel.FeedbackChannelRatingCategoryDTO
import no.javazone.feedback.request.channel.FeedbackChannelUpdateDTO
import no.javazone.feedback.request.channel.FeedbackCreationDTO
import no.javazone.feedback.request.channel.FeedbackDTO
import no.javazone.feedback.request.channel.FeedbackRatingDTO
Expand Down Expand Up @@ -99,6 +103,11 @@ fun Application.setupRouting() {
HttpStatusCode.NotFound,
e.message
)
} catch (e: ChannelClosedError) {
return@post call.respond(
HttpStatusCode.Forbidden,
e.message
)
}

val feedbackDto = createdFeedback.let { feedbackWithComment ->
Expand Down Expand Up @@ -126,6 +135,30 @@ fun Application.setupRouting() {
call.respond(feedbackDto)
}

patch("{channelId}") {
val channelId = call.parameters["channelId"] ?: return@patch call.respond(
HttpStatusCode.NotFound,
"Missing externalId"
)
val updateInput = call.receive<FeedbackChannelUpdateDTO>()
val existing = feedbackAdapter.findChannel(channelId)
?: return@patch call.respond(HttpStatusCode.NotFound, "Channel with id $channelId not found.")
val merged = FeedbackChannel(
id = existing.id,
title = existing.title,
speakers = existing.speakers,
externalId = existing.externalId,
ratingCategories = existing.ratingCategories,
isOpen = updateInput.isOpen ?: existing.isOpen
)
val updated = try {
feedbackAdapter.updateChannel(merged)
} catch (e: ChannelNotFoundError) {
return@patch call.respond(HttpStatusCode.NotFound, e.message)
}
call.respond(updated.toDTO())
}

get("{channelId}/qrcode") {
val channelId = call.parameters["channelId"] ?: return@get call.respond(
HttpStatusCode.NotFound,
Expand Down
138 changes: 136 additions & 2 deletions core/src/test/kotlin/no/javazone/feedback/FeedbackEndpointsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package no.javazone.feedback
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.client.statement.*import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -74,6 +73,131 @@ class FeedbackEndpointsTest {
assertEquals(2, responseBody.ratingCategories.size)
}

@Test
fun `newly created channel is closed by default`() = testApplication {
application {
module(TestDatabase.config())
}

val client = createClient {
install(ContentNegotiation) {
json()
}
}

val channel = client.post("/v1/feedback/channel") {
contentType(ContentType.Application.Json)
setBody(
FeedbackChannelCreationDTO(
title = "Closed by default",
speakers = listOf("Speaker"),
ratingCategories = listOf(FeedbackChannelRatingCategoryDTO(id = null, title = "Rating"))
)
)
}.body<FeedbackChannelDTO>()

assertEquals(false, channel.isOpen)
}

@Test
fun `submitting feedback to closed channel returns forbidden`() = testApplication {
application {
module(TestDatabase.config())
}

val client = createClient {
install(ContentNegotiation) {
json()
}
}

val channel = client.post("/v1/feedback/channel") {
contentType(ContentType.Application.Json)
setBody(
FeedbackChannelCreationDTO(
title = "Closed",
speakers = listOf("Speaker"),
ratingCategories = listOf(FeedbackChannelRatingCategoryDTO(id = null, title = "Rating"))
)
)
}.body<FeedbackChannelDTO>()

val response = client.post("/v1/feedback/channel/${channel.channelId}/submit-feedback") {
contentType(ContentType.Application.Json)
setBody(
FeedbackCreationDTO(
ratings = listOf(FeedbackRatingCreationDTO(id = channel.ratingCategories[0].id!!, score = 5)),
detailedComment = null
)
)
}

assertEquals(HttpStatusCode.Forbidden, response.status)
}

@Test
fun `patch channel opens the channel and allows submissions`() = testApplication {
application {
module(TestDatabase.config())
}

val client = createClient {
install(ContentNegotiation) {
json()
}
}

val channel = client.post("/v1/feedback/channel") {
contentType(ContentType.Application.Json)
setBody(
FeedbackChannelCreationDTO(
title = "Toggle me",
speakers = listOf("Speaker"),
ratingCategories = listOf(FeedbackChannelRatingCategoryDTO(id = null, title = "Rating"))
)
)
}.body<FeedbackChannelDTO>()

val patched = client.patch("/v1/feedback/channel/${channel.channelId}") {
contentType(ContentType.Application.Json)
setBody(FeedbackChannelUpdateDTO(isOpen = true))
}

assertEquals(HttpStatusCode.OK, patched.status)
assertEquals(true, patched.body<FeedbackChannelDTO>().isOpen)

val submit = client.post("/v1/feedback/channel/${channel.channelId}/submit-feedback") {
contentType(ContentType.Application.Json)
setBody(
FeedbackCreationDTO(
ratings = listOf(FeedbackRatingCreationDTO(id = channel.ratingCategories[0].id!!, score = 5)),
detailedComment = null
)
)
}
assertEquals(HttpStatusCode.OK, submit.status)
}

@Test
fun `patch channel returns not found for unknown channel`() = testApplication {
application {
module(TestDatabase.config())
}

val client = createClient {
install(ContentNegotiation) {
json()
}
}

val response = client.patch("/v1/feedback/channel/ZZZZ") {
contentType(ContentType.Application.Json)
setBody(FeedbackChannelUpdateDTO(isOpen = true))
}

assertEquals(HttpStatusCode.NotFound, response.status)
}

@Test
fun `test submit feedback successfully`() = testApplication {
application {
Expand Down Expand Up @@ -103,6 +227,11 @@ class FeedbackEndpointsTest {

val channelId = channel.channelId

client.patch("/v1/feedback/channel/$channelId") {
contentType(ContentType.Application.Json)
setBody(FeedbackChannelUpdateDTO(isOpen = true))
}

// Now submit feedback
val feedbackCreationDto = FeedbackCreationDTO(
ratings = listOf(
Expand Down Expand Up @@ -183,6 +312,11 @@ class FeedbackEndpointsTest {
val channel = createChannelResponse.body<FeedbackChannelDTO>()
val channelId = channel.channelId

client.patch("/v1/feedback/channel/$channelId") {
contentType(ContentType.Application.Json)
setBody(FeedbackChannelUpdateDTO(isOpen = true))
}

// Submit feedback without comment
val feedbackCreationDto = FeedbackCreationDTO(
ratings = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ object FeedbackChannels : LongIdTable("feedback_channel") {
val title = varchar("title", 255)
val speakers = array<String>("speakers")
val externalId = varchar("external_id", 255)
val isOpen = bool("is_open").default(false)
val createdAt = timestamp("created_at")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insertReturning
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.postgresql.util.PSQLState
import java.time.Instant

Expand All @@ -27,6 +28,7 @@ object FeedbackRepositoryDb : FeedbackRepository {
it[title] = channel.title
it[speakers] = channel.speakers
it[externalId] = channel.externalId
it[isOpen] = channel.isOpen
}.map {
it[FeedbackChannels.id]
}.first()
Expand All @@ -50,7 +52,8 @@ object FeedbackRepositoryDb : FeedbackRepository {
title = it[FeedbackChannels.title],
speakers = it[FeedbackChannels.speakers],
externalId = it[FeedbackChannels.externalId],
ratingCategories = ratingCategories
ratingCategories = ratingCategories,
isOpen = it[FeedbackChannels.isOpen]
)
}.first()
}
Expand Down Expand Up @@ -106,7 +109,8 @@ object FeedbackRepositoryDb : FeedbackRepository {
title = firstRow[FeedbackChannels.title],
speakers = firstRow[FeedbackChannels.speakers],
externalId = firstRow[FeedbackChannels.externalId],
ratingCategories = emptyList()
ratingCategories = emptyList(),
isOpen = firstRow[FeedbackChannels.isOpen]
)
}
}
Expand Down Expand Up @@ -138,9 +142,21 @@ object FeedbackRepositoryDb : FeedbackRepository {
title = firstRow[FeedbackChannels.title],
speakers = firstRow[FeedbackChannels.speakers],
externalId = firstRow[FeedbackChannels.externalId],
ratingCategories = ratingCategories
ratingCategories = ratingCategories,
isOpen = firstRow[FeedbackChannels.isOpen]
)
}
}
}

override fun updateChannel(channel: FeedbackChannel): FeedbackChannel? {
return transaction {
val updated = FeedbackChannels.update({ FeedbackChannels.id eq channel.id }) {
it[title] = channel.title
it[speakers] = channel.speakers
it[isOpen] = channel.isOpen
}
if (updated == 0) null else findByChannelId(channel.externalId)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--liquibase formatted sql

--changeset tanettrimas:8
ALTER TABLE feedback_channel ADD COLUMN is_open BOOLEAN NOT NULL DEFAULT FALSE;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ class FeedbackChannel(
val title: String,
val speakers: List<String>,
val externalId: String,
val ratingCategories: List<FeedbackChannelRatingCategory>
val ratingCategories: List<FeedbackChannelRatingCategory>,
val isOpen: Boolean = false
) {
init {
require(speakers.all { it.isNotEmpty() }) { "All speakers must not be empty." }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import no.javazone.feedback.domain.Feedback
import no.javazone.feedback.domain.FeedbackChannel
import no.javazone.feedback.domain.FeedbackChannelCreationInput
import no.javazone.feedback.domain.FeedbackWithChannel
import no.javazone.feedback.domain.errors.ChannelClosedError
import no.javazone.feedback.domain.errors.ChannelNotFoundError
import no.javazone.feedback.domain.errors.ExternalIdAlreadyExistsError
import no.javazone.feedback.domain.errors.ExternalIdGenerationException
Expand Down Expand Up @@ -37,14 +38,22 @@ class FeedbackAdapter(

fun submitFeedback(channelId: String, feedback: Feedback): FeedbackWithChannel {
val feedbackChannel = repository.findByChannelId(channelId)
?: throw ChannelNotFoundError("Channel with id $channelId does not exist")
?: throw ChannelNotFoundError(channelId)
if (!feedbackChannel.isOpen) {
throw ChannelClosedError(channelId)
}
val createdFeedback = repository.submitFeedback(feedback, feedbackChannel)
return FeedbackWithChannel(
channel = feedbackChannel,
feedback = createdFeedback
)
}

fun updateChannel(channel: FeedbackChannel): FeedbackChannel {
return repository.updateChannel(channel)
?: throw ChannelNotFoundError(channel.externalId)
}

fun findChannel(channelId: String): FeedbackChannel? {
return repository.findByChannelId(channelId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ sealed class DomainErrors(
class ChannelNotFoundError(channelId: String) :
DomainErrors("Channel with id $channelId not found.")

class ChannelClosedError(channelId: String) :
DomainErrors("Channel with id $channelId is closed for feedback.")

class ExternalIdAlreadyExistsError(externalId: String, cause: Throwable? = null) :
DomainErrors("Channel with external id $externalId already exists.", cause)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ interface FeedbackRepository {
fun submitFeedback(feedback: Feedback, feedbackChannel: FeedbackChannel): Feedback
fun findByChannelId(channelId: String): FeedbackChannel?
fun findAllChannels(): List<FeedbackChannel>
fun updateChannel(channel: FeedbackChannel): FeedbackChannel?
}
Loading