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 @@ -49,7 +49,7 @@ fun Route.blockedRoutes(blockedService: BlockedService, authService: AuthService
}
delete("/blocked/videos/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val role = authService.getUserRole(userId) ?: "user"
val deleted = blockedService.deleteVideo(userId, videoUrl, role)
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound, ErrorResponse("Not found"))
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/dev/typetype/server/routes/FavoritesRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ fun Route.favoritesRoutes(favoritesService: FavoritesService, authService: AuthS
}
post("/favorites/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
call.respond(HttpStatusCode.Created, favoritesService.add(userId, videoUrl))
}
}
delete("/favorites/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val deleted = favoritesService.delete(userId, videoUrl)
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound, ErrorResponse("Not found"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fun Route.playlistRoutes(playlistService: PlaylistService, authService: AuthServ
delete("/playlists/{id}/videos/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val id = call.parameters["id"] ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing id"))
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val deleted = playlistService.removeVideo(userId, id, videoUrl)
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound, ErrorResponse("Not found"))
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/dev/typetype/server/routes/ProgressRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal data class ProgressBody(val position: Long)
fun Route.progressRoutes(progressService: ProgressService, authService: AuthService) {
get("/progress/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val item = progressService.get(userId, videoUrl) ?: return@withJwtAuth call.respond(HttpStatusCode.NotFound, ErrorResponse("Not found"))
call.respond(item)
}
Expand All @@ -32,7 +32,7 @@ fun Route.progressRoutes(progressService: ProgressService, authService: AuthServ
}
put("/progress/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val videoUrl = call.parameters.getAll("videoUrl")?.joinToString("/") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val videoUrl = call.urlTailParameter("videoUrl") ?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val body = runCatching { call.receive<ProgressBody>() }.getOrElse {
return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Invalid request body"))
}
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/dev/typetype/server/routes/RouteUrlParameter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.typetype.server.routes

import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.queryString

internal fun ApplicationCall.urlTailParameter(name: String): String? {
val pathUrl = parameters.getAll(name)?.joinToString("/") ?: return null
val normalizedPathUrl = pathUrl.withUrlSchemeSlashes()
val queryString = request.queryString()
return if (queryString.isBlank() || "?" in normalizedPathUrl) {
normalizedPathUrl
} else {
"$normalizedPathUrl?$queryString"
}
}

private fun String.withUrlSchemeSlashes(): String = when {
startsWith("https:/") && !startsWith("https://") -> "https://" + removePrefix("https:/")
startsWith("http:/") && !startsWith("http://") -> "http://" + removePrefix("http:/")
else -> this
}
16 changes: 1 addition & 15 deletions src/main/kotlin/dev/typetype/server/routes/WatchLaterRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import dev.typetype.server.models.WatchLaterItem
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.WatchLaterService
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.queryString
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
Expand All @@ -27,15 +26,8 @@ fun Route.watchLaterRoutes(watchLaterService: WatchLaterService, authService: Au
}
delete("/watch-later/{videoUrl...}") {
call.withJwtAuth(authService) { userId ->
val pathVideoUrl = call.parameters.getAll("videoUrl")?.joinToString("/")
val videoUrl = call.urlTailParameter("videoUrl")
?: return@withJwtAuth call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing videoUrl"))
val decodedPathVideoUrl = pathVideoUrl.withUrlSchemeSlashes()
val queryString = call.request.queryString()
val videoUrl = if (queryString.isBlank() || "?" in decodedPathVideoUrl) {
decodedPathVideoUrl
} else {
"$decodedPathVideoUrl?$queryString"
}
val deleted = watchLaterService.delete(userId, videoUrl)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
Expand All @@ -45,9 +37,3 @@ fun Route.watchLaterRoutes(watchLaterService: WatchLaterService, authService: Au
}
}
}

private fun String.withUrlSchemeSlashes(): String = when {
startsWith("https:/") && !startsWith("https://") -> "https://" + removePrefix("https:/")
startsWith("http:/") && !startsWith("http://") -> "http://" + removePrefix("http:/")
else -> this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package dev.typetype.server

import dev.typetype.server.models.PlaylistItem
import dev.typetype.server.routes.favoritesRoutes
import dev.typetype.server.routes.playlistRoutes
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.FavoritesService
import dev.typetype.server.services.PlaylistService
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class EncodedVideoUrlDeleteRoutesTest {

private val auth = AuthService.fixed(TEST_USER_ID)
private val favoritesService = FavoritesService()
private val playlistService = PlaylistService()

companion object {
private const val VIDEO_URL = "https://www.youtube.com/watch?v=ZZZZZZZZZZZ"
private const val ENCODED_VIDEO_URL = "https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZZZZZZZZZZZ"

@BeforeAll
@JvmStatic
fun initDb() { TestDatabase.setup() }
}

@BeforeEach
fun clean() { TestDatabase.truncateAll() }

private fun withApp(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
application {
install(ContentNegotiation) { json() }
routing {
favoritesRoutes(favoritesService, auth)
playlistRoutes(playlistService, auth)
}
}
block()
}

@Test
fun `DELETE favorites removes encoded youtube watch url added by POST`() = withApp {
val post = client.post("/favorites/$ENCODED_VIDEO_URL") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}
assertEquals(HttpStatusCode.Created, post.status)
assertTrue(post.bodyAsText().contains("\"videoUrl\":\"$VIDEO_URL\""))

val delete = client.delete("/favorites/$ENCODED_VIDEO_URL") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}
assertEquals(HttpStatusCode.NoContent, delete.status)
}

@Test
fun `DELETE playlists video removes encoded youtube watch url added by POST`() = withApp {
val playlist = playlistService.create(TEST_USER_ID, PlaylistItem(name = "Favorites"))
val body = """{"url":"$VIDEO_URL","title":"test","thumbnail":"","duration":0}"""
val post = client.post("/playlists/${playlist.id}/videos") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(body)
}
assertEquals(HttpStatusCode.Created, post.status)

val delete = client.delete("/playlists/${playlist.id}/videos/$ENCODED_VIDEO_URL") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}
assertEquals(HttpStatusCode.NoContent, delete.status)
val afterDelete = client.get("/playlists/${playlist.id}") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}.bodyAsText()
assertFalse(afterDelete.contains(VIDEO_URL))
}
}