feat: restrict content by labels

This commit is contained in:
Gauthier Roebroeck 2022-02-24 17:59:06 +08:00
parent 496ebb0aac
commit 8d4eb68f7d
21 changed files with 1570 additions and 141 deletions

View file

@ -0,0 +1,22 @@
package org.gotson.komga.domain.model
import org.gotson.komga.language.lowerNotBlank
import org.gotson.komga.language.toSetOrNull
class ContentRestrictions(
val ageRestriction: ContentRestriction.AgeRestriction? = null,
labelsAllow: Set<String> = emptySet(),
labelsExclude: Set<String> = emptySet(),
) {
val labelsAllowRestriction =
labelsAllow.lowerNotBlank().toSet()
.minus(labelsExclude.lowerNotBlank().toSet())
.toSetOrNull()?.let { ContentRestriction.LabelsRestriction.AllowOnly(it) }
val labelsExcludeRestriction =
labelsExclude.lowerNotBlank().toSetOrNull()?.let { ContentRestriction.LabelsRestriction.Exclude(it) }
fun isRestricted() = ageRestriction != null || labelsAllowRestriction != null || labelsExcludeRestriction != null
override fun toString(): String = "ContentRestriction(ageRestriction=$ageRestriction, labelsAllowRestriction=$labelsAllowRestriction, labelsExcludeRestriction=$labelsExcludeRestriction)"
}

View file

@ -1,6 +1,7 @@
package org.gotson.komga.domain.model
import com.github.f4b6a3.tsid.TsidCreator
import org.gotson.komga.language.lowerNotBlank
import java.io.Serializable
import java.time.LocalDateTime
import javax.validation.constraints.Email
@ -22,7 +23,7 @@ data class KomgaUser(
val rolePageStreaming: Boolean = true,
val sharedLibrariesIds: Set<String> = emptySet(),
val sharedAllLibraries: Boolean = true,
val restrictions: Set<ContentRestriction> = emptySet(),
val restrictions: ContentRestrictions = ContentRestrictions(),
val id: String = TsidCreator.getTsid256().toString(),
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = createdDate,
@ -67,16 +68,37 @@ data class KomgaUser(
return sharedAllLibraries || sharedLibrariesIds.any { it == library.id }
}
fun isContentRestricted(ageRating: Int? = null, sharingLabels: Set<String> = emptySet()): Boolean {
restrictions.forEach { restriction ->
when (restriction) {
is ContentRestriction.AgeRestriction.AllowOnlyUnder -> if (ageRating == null || ageRating > restriction.age) return true
is ContentRestriction.AgeRestriction.ExcludeOver -> ageRating?.let { if (it >= restriction.age) return true }
is ContentRestriction.LabelsRestriction.AllowOnly -> if (restriction.labels.intersect(sharingLabels).isEmpty()) return true
is ContentRestriction.LabelsRestriction.Exclude -> if (restriction.labels.intersect(sharingLabels).isNotEmpty()) return true
}
fun isContentAllowed(ageRating: Int? = null, sharingLabels: Set<String> = emptySet()): Boolean {
val labels = sharingLabels.lowerNotBlank().toSet()
val ageAllowed =
if (restrictions.ageRestriction is ContentRestriction.AgeRestriction.AllowOnlyUnder)
ageRating != null && ageRating <= restrictions.ageRestriction.age
else null
val labelAllowed =
if (restrictions.labelsAllowRestriction != null)
restrictions.labelsAllowRestriction.labels.intersect(labels).isNotEmpty()
else null
val allowed = when {
ageAllowed == null -> labelAllowed != false
labelAllowed == null -> ageAllowed != false
else -> ageAllowed != false || labelAllowed != false
}
return false
if (!allowed) return false
val ageDenied =
if (restrictions.ageRestriction is ContentRestriction.AgeRestriction.ExcludeOver)
ageRating != null && ageRating >= restrictions.ageRestriction.age
else false
val labelDenied =
if (restrictions.labelsExcludeRestriction != null)
restrictions.labelsExcludeRestriction.labels.intersect(labels).isNotEmpty()
else false
return !ageDenied && !labelDenied
}
override fun toString(): String {

View file

@ -1,6 +1,6 @@
package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.ReadList
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
@ -10,20 +10,20 @@ interface ReadListRepository {
* Find one ReadList by [readListId],
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] it not null.
*/
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: Set<ContentRestriction> = emptySet()): ReadList?
fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): ReadList?
/**
* Find all ReadList
* optionally with at least one Book belonging to the provided [belongsToLibraryIds] if not null,
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<ReadList>
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<ReadList>
/**
* Find all ReadList that contains the provided [containsBookId],
* optionally with only bookIds filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): Collection<ReadList>
fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<ReadList>
fun findAllEmpty(): Collection<ReadList>

View file

@ -1,6 +1,6 @@
package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.SeriesCollection
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
@ -10,20 +10,20 @@ interface SeriesCollectionRepository {
* Find one SeriesCollection by [collectionId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: Set<ContentRestriction> = emptySet()): SeriesCollection?
fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>? = null, restrictions: ContentRestrictions = ContentRestrictions()): SeriesCollection?
/**
* Find all SeriesCollection
* optionally with at least one Series belonging to the provided [belongsToLibraryIds] if not null,
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesCollection>
fun findAll(belongsToLibraryIds: Collection<String>? = null, filterOnLibraryIds: Collection<String>? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<SeriesCollection>
/**
* Find all SeriesCollection that contains the provided [containsSeriesId],
* optionally with only seriesId filtered by the provided [filterOnLibraryIds] if not null.
*/
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): Collection<SeriesCollection>
fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): Collection<SeriesCollection>
fun findAllEmpty(): Collection<SeriesCollection>

View file

@ -1,7 +1,7 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.ReadStatus
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
import org.gotson.komga.infrastructure.search.LuceneEntity
@ -78,8 +78,8 @@ class BookDtoDao(
"readList.number" to rlb.NUMBER,
)
override fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<BookDto> {
val conditions = search.toCondition().and(restrictions.toCondition())
override fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions): Page<BookDto> {
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
return findAll(conditions, userId, pageable, search.toJoinConditions(), null, search.searchTerm)
}
@ -174,13 +174,13 @@ class BookDtoDao(
): BookDto? =
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true)
override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<BookDto> {
override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: ContentRestrictions): Page<BookDto> {
val seriesIds = dsl.select(s.ID)
.from(s)
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
.leftJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
.where(restrictions.toCondition())
.where(restrictions.toCondition(dsl))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.groupBy(s.ID)
.having(countUnread.ge(inline(1.toBigDecimal())))

View file

@ -1,6 +1,6 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.persistence.ReadListRepository
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
@ -39,31 +39,31 @@ class ReadListDao(
"name" to rl.NAME.collate(SqliteUdfDataSource.collationUnicode3),
)
override fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): ReadList? =
selectBase(restrictions.isNotEmpty())
override fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions): ReadList? =
selectBase(restrictions.isRestricted())
.where(rl.ID.eq(readListId))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.fetchAndMap(filterOnLibraryIds, restrictions)
.firstOrNull()
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<ReadList> {
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: ContentRestrictions): Page<ReadList> {
val readListIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.ReadList)
val searchCondition = rl.ID.inOrNoCondition(readListIds)
val conditions = searchCondition
.and(b.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
.and(b.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
.and(restrictions.toCondition())
.and(restrictions.toCondition(dsl))
val queryIds =
if (belongsToLibraryIds == null && filterOnLibraryIds == null && restrictions.isEmpty()) null
if (belongsToLibraryIds == null && filterOnLibraryIds == null && !restrictions.isRestricted()) null
else
dsl.selectDistinct(rl.ID)
.from(rl)
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.apply { if (restrictions.isRestricted()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.where(conditions)
val count =
@ -76,7 +76,7 @@ class ReadListDao(
else it.toSortField(sorts)
}
val items = selectBase(restrictions.isNotEmpty())
val items = selectBase(restrictions.isRestricted())
.where(conditions)
.apply { if (queryIds != null) and(rl.ID.`in`(queryIds)) }
.orderBy(orderBy)
@ -92,18 +92,18 @@ class ReadListDao(
)
}
override fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): Collection<ReadList> {
override fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions): Collection<ReadList> {
val queryIds = dsl.select(rl.ID)
.from(rl)
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
.apply { if (restrictions.isNotEmpty()) leftJoin(b).on(rlb.BOOK_ID.eq(b.ID)).leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.apply { if (restrictions.isRestricted()) leftJoin(b).on(rlb.BOOK_ID.eq(b.ID)).leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.where(rlb.BOOK_ID.eq(containsBookId))
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
return selectBase(restrictions.isNotEmpty())
return selectBase(restrictions.isRestricted())
.where(rl.ID.`in`(queryIds))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.fetchAndMap(filterOnLibraryIds, restrictions)
}
@ -132,16 +132,16 @@ class ReadListDao(
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
.apply { if (joinOnSeriesMetadata) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): List<ReadList> =
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): List<ReadList> =
fetchInto(rl)
.map { rr ->
val bookIds = dsl.select(*rlb.fields())
.from(rlb)
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.apply { if (restrictions.isRestricted()) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
.where(rlb.READLIST_ID.eq(rr.id))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.orderBy(rlb.NUMBER.asc())
.fetchInto(rlb)
.mapNotNull { it.number to it.bookId }

View file

@ -1,6 +1,6 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
@ -38,25 +38,25 @@ class SeriesCollectionDao(
"name" to c.NAME.collate(SqliteUdfDataSource.collationUnicode3),
)
override fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): SeriesCollection? =
selectBase(restrictions.isNotEmpty())
override fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions): SeriesCollection? =
selectBase(restrictions.isRestricted())
.where(c.ID.eq(collectionId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.fetchAndMap(filterOnLibraryIds, restrictions)
.firstOrNull()
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<SeriesCollection> {
override fun findAll(belongsToLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?, search: String?, pageable: Pageable, restrictions: ContentRestrictions): Page<SeriesCollection> {
val collectionIds = luceneHelper.searchEntitiesIds(search, LuceneEntity.Collection)
val searchCondition = c.ID.inOrNoCondition(collectionIds)
val conditions = searchCondition
.and(s.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
.and(s.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
.and(restrictions.toCondition())
.and(restrictions.toCondition(dsl))
val queryIds =
if (belongsToLibraryIds == null && filterOnLibraryIds == null && restrictions.isEmpty()) null
if (belongsToLibraryIds == null && filterOnLibraryIds == null && !restrictions.isRestricted()) null
else
dsl.selectDistinct(c.ID)
.from(c)
@ -75,7 +75,7 @@ class SeriesCollectionDao(
else it.toSortField(sorts)
}
val items = selectBase(restrictions.isNotEmpty())
val items = selectBase(restrictions.isRestricted())
.where(conditions)
.apply { if (queryIds != null) and(c.ID.`in`(queryIds)) }
.orderBy(orderBy)
@ -91,18 +91,18 @@ class SeriesCollectionDao(
)
}
override fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction>): Collection<SeriesCollection> {
override fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions): Collection<SeriesCollection> {
val queryIds = dsl.select(c.ID)
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
.apply { if (restrictions.isRestricted()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
.where(cs.SERIES_ID.eq(containsSeriesId))
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
return selectBase(restrictions.isNotEmpty())
return selectBase(restrictions.isRestricted())
.where(c.ID.`in`(queryIds))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.fetchAndMap(filterOnLibraryIds, restrictions)
}
@ -131,16 +131,16 @@ class SeriesCollectionDao(
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.apply { if (joinOnSeriesMetadata) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: Set<ContentRestriction> = emptySet()): List<SeriesCollection> =
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<String>?, restrictions: ContentRestrictions = ContentRestrictions()): List<SeriesCollection> =
fetchInto(c)
.map { cr ->
val seriesIds = dsl.select(*cs.fields())
.from(cs)
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.apply { if (restrictions.isNotEmpty()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
.apply { if (restrictions.isRestricted()) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
.where(cs.COLLECTION_ID.eq(cr.id))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.apply { if (restrictions.isNotEmpty()) and(restrictions.toCondition()) }
.apply { if (restrictions.isRestricted()) and(restrictions.toCondition(dsl)) }
.orderBy(cs.NUMBER.asc())
.fetchInto(cs)
.mapNotNull { it.seriesId }

View file

@ -1,7 +1,7 @@
package org.gotson.komga.infrastructure.jooq
import mu.KotlinLogging
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.ReadStatus
import org.gotson.komga.domain.model.SeriesSearch
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
@ -26,6 +26,7 @@ import org.jooq.ResultQuery
import org.jooq.SelectOnConditionStep
import org.jooq.impl.DSL
import org.jooq.impl.DSL.count
import org.jooq.impl.DSL.countDistinct
import org.jooq.impl.DSL.lower
import org.jooq.impl.DSL.substring
import org.springframework.data.domain.Page
@ -78,8 +79,8 @@ class SeriesDtoDao(
"booksCount" to s.BOOK_COUNT,
)
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction>): Page<SeriesDto> {
val conditions = search.toCondition().and(restrictions.toCondition())
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions): Page<SeriesDto> {
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
}
@ -89,9 +90,9 @@ class SeriesDtoDao(
search: SeriesSearchWithReadProgress,
userId: String,
pageable: Pageable,
restrictions: Set<ContentRestriction>,
restrictions: ContentRestrictions,
): Page<SeriesDto> {
val conditions = search.toCondition().and(restrictions.toCondition()).and(cs.COLLECTION_ID.eq(collectionId))
val conditions = search.toCondition().and(restrictions.toCondition(dsl)).and(cs.COLLECTION_ID.eq(collectionId))
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
return findAll(conditions, userId, pageable, joinConditions, search.searchTerm)
@ -100,18 +101,18 @@ class SeriesDtoDao(
override fun findAllRecentlyUpdated(
search: SeriesSearchWithReadProgress,
userId: String,
restrictions: Set<ContentRestriction>,
restrictions: ContentRestrictions,
pageable: Pageable,
): Page<SeriesDto> {
val conditions = search.toCondition()
.and(restrictions.toCondition())
.and(restrictions.toCondition(dsl))
.and(s.CREATED_DATE.notEqual(s.LAST_MODIFIED_DATE))
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
}
override fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>): List<GroupCountDto> {
val conditions = search.toCondition().and(restrictions.toCondition())
override fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions): List<GroupCountDto> {
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
val joinConditions = search.toJoinConditions()
val seriesIds = luceneHelper.searchEntitiesIds(search.searchTerm, LuceneEntity.Series)
val searchCondition = s.ID.inOrNoCondition(seriesIds)
@ -174,7 +175,7 @@ class SeriesDtoDao(
val seriesIds = luceneHelper.searchEntitiesIds(searchTerm, LuceneEntity.Series)
val searchCondition = s.ID.inOrNoCondition(seriesIds)
val count = dsl.select(count(s.ID))
val count = dsl.select(countDistinct(s.ID))
.from(s)
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
@ -189,7 +190,7 @@ class SeriesDtoDao(
.apply { if (joinConditions.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
.where(conditions)
.and(searchCondition)
.fetchOne(count(s.ID)) ?: 0
.fetchOne(countDistinct(s.ID)) ?: 0
val orderBy =
pageable.sort.mapNotNull {

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
import org.gotson.komga.jooq.Tables
import org.jooq.Condition
@ -65,15 +66,34 @@ fun DSLContext.insertTempStrings(batchSize: Int, collection: Collection<String>)
fun DSLContext.selectTempStrings() = this.select(Tables.TEMP_STRING_LIST.STRING).from(Tables.TEMP_STRING_LIST)
fun Set<ContentRestriction>.toCondition(): Condition =
this.fold(DSL.noCondition()) { accumulator, restriction ->
accumulator.and(
when (restriction) {
is ContentRestriction.AgeRestriction.AllowOnlyUnder -> Tables.SERIES_METADATA.AGE_RATING.lessOrEqual(restriction.age)
is ContentRestriction.AgeRestriction.ExcludeOver -> Tables.SERIES_METADATA.AGE_RATING.isNull.or(Tables.SERIES_METADATA.AGE_RATING.lessThan(restriction.age))
// TODO: add conditions for sharing labels
is ContentRestriction.LabelsRestriction.AllowOnly -> DSL.noCondition()
is ContentRestriction.LabelsRestriction.Exclude -> DSL.noCondition()
},
)
}
fun ContentRestrictions.toCondition(dsl: DSLContext): Condition {
val ageAllowed = if (ageRestriction is ContentRestriction.AgeRestriction.AllowOnlyUnder) {
Tables.SERIES_METADATA.AGE_RATING.isNotNull.and(Tables.SERIES_METADATA.AGE_RATING.lessOrEqual(ageRestriction.age))
} else DSL.noCondition()
val labelAllowed =
if (labelsAllowRestriction != null)
Tables.SERIES_METADATA.SERIES_ID.`in`(
dsl.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
.from(Tables.SERIES_METADATA_SHARING)
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(labelsAllowRestriction.labels)),
)
else DSL.noCondition()
val ageDenied =
if (ageRestriction is ContentRestriction.AgeRestriction.ExcludeOver)
Tables.SERIES_METADATA.AGE_RATING.isNull.or(Tables.SERIES_METADATA.AGE_RATING.lessThan(ageRestriction.age))
else DSL.noCondition()
val labelDenied =
if (labelsExcludeRestriction != null)
Tables.SERIES_METADATA.SERIES_ID.notIn(
dsl.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
.from(Tables.SERIES_METADATA_SHARING)
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(labelsExcludeRestriction.labels)),
)
else DSL.noCondition()
return ageAllowed.or(labelAllowed)
.and(ageDenied.and(labelDenied))
}

View file

@ -12,5 +12,5 @@ import org.springframework.web.server.ResponseStatusException
*/
fun KomgaUser.checkContentRestriction(series: SeriesDto) {
if (!canAccessLibrary(series.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (isContentRestricted(ageRating = series.metadata.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (!isContentAllowed(series.metadata.ageRating, series.metadata.sharingLabels)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
}

View file

@ -1,13 +1,13 @@
package org.gotson.komga.interfaces.api.persistence
import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.interfaces.api.rest.dto.BookDto
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
interface BookDtoRepository {
fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<BookDto>
fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<BookDto>
/**
* Find books that are part of a readlist, optionally filtered by library
@ -39,7 +39,7 @@ interface BookDtoRepository {
filterOnLibraryIds: Collection<String>?,
): BookDto?
fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<BookDto>
fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<BookDto>
fun findAllDuplicates(userId: String, pageable: Pageable): Page<BookDto>
}

View file

@ -1,6 +1,6 @@
package org.gotson.komga.interfaces.api.persistence
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
import org.gotson.komga.interfaces.api.rest.dto.GroupCountDto
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
@ -10,9 +10,9 @@ import org.springframework.data.domain.Pageable
interface SeriesDtoRepository {
fun findByIdOrNull(seriesId: String, userId: String): SeriesDto?
fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesDto>
fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set<ContentRestriction> = emptySet()): Page<SeriesDto>
fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>, pageable: Pageable): Page<SeriesDto>
fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<SeriesDto>
fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page<SeriesDto>
fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions, pageable: Pageable): Page<SeriesDto>
fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set<ContentRestriction>): List<GroupCountDto>
fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions): List<GroupCountDto>
}

View file

@ -691,8 +691,8 @@ class BookController(
*/
private fun KomgaUser.checkContentRestriction(book: BookDto) {
if (!canAccessLibrary(book.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(book.seriesId).let {
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (restrictions.isRestricted()) seriesMetadataRepository.findById(book.seriesId).let {
if (!isContentAllowed(it.ageRating, it.sharingLabels)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
}
}
@ -704,8 +704,8 @@ class BookController(
*/
private fun KomgaUser.checkContentRestriction(book: Book) {
if (!canAccessLibrary(book.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(book.seriesId).let {
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (restrictions.isRestricted()) seriesMetadataRepository.findById(book.seriesId).let {
if (!isContentAllowed(it.ageRating, it.sharingLabels)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
}
}
@ -721,9 +721,9 @@ class BookController(
if (!canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
if (restrictions.isNotEmpty()) bookRepository.getSeriesIdOrNull(bookId)?.let { seriesId ->
if (restrictions.isRestricted()) bookRepository.getSeriesIdOrNull(bookId)?.let { seriesId ->
seriesMetadataRepository.findById(seriesId).let {
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (!isContentAllowed(it.ageRating, it.sharingLabels)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

View file

@ -699,8 +699,8 @@ class SeriesController(
if (!canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
if (restrictions.isNotEmpty()) seriesMetadataRepository.findById(seriesId).let {
if (isContentRestricted(ageRating = it.ageRating)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
if (restrictions.isRestricted()) seriesMetadataRepository.findById(seriesId).let {
if (!isContentAllowed(it.ageRating, it.sharingLabels)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
}
}
}

View file

@ -0,0 +1,91 @@
package org.gotson.komga.domain.model
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class ContentRestrictionsTest {
@Test
fun `given no arguments when creating restriction then all restrictions are null`() {
val restriction = ContentRestrictions()
assertThat(restriction.ageRestriction).isNull()
assertThat(restriction.labelsAllowRestriction).isNull()
assertThat(restriction.labelsExcludeRestriction).isNull()
}
@Test
fun `given AllowOnlyUnder restriction only when creating restriction then label restrictions are null`() {
val restriction = ContentRestrictions(ContentRestriction.AgeRestriction.AllowOnlyUnder(10))
assertThat(restriction.ageRestriction)
.isNotNull
.isInstanceOf(ContentRestriction.AgeRestriction.AllowOnlyUnder::class.java)
assertThat(restriction.ageRestriction!!.age).isEqualTo(10)
assertThat(restriction.labelsAllowRestriction).isNull()
assertThat(restriction.labelsExcludeRestriction).isNull()
}
@Test
fun `given ExcludeOver only when creating restriction then label restrictions are null`() {
val restriction = ContentRestrictions(ContentRestriction.AgeRestriction.ExcludeOver(10))
assertThat(restriction.ageRestriction)
.isNotNull
.isInstanceOf(ContentRestriction.AgeRestriction.ExcludeOver::class.java)
assertThat(restriction.ageRestriction!!.age).isEqualTo(10)
assertThat(restriction.labelsAllowRestriction).isNull()
assertThat(restriction.labelsExcludeRestriction).isNull()
}
@Test
fun `given empty labels when creating restriction then label restrictions are normalized`() {
val restriction = ContentRestrictions(
labelsAllow = setOf("", " "),
labelsExclude = setOf("", " "),
)
assertThat(restriction.labelsAllowRestriction).isNull()
assertThat(restriction.labelsExcludeRestriction).isNull()
}
@Test
fun `given labels with duplicate values when creating restriction then label restrictions are normalized`() {
val restriction = ContentRestrictions(
labelsAllow = setOf("a", "b", "B", "b ", "b", " B "),
labelsExclude = setOf("c", "d", "D", "d ", "d", " D ")
)
assertThat(restriction.labelsAllowRestriction).isNotNull
assertThat(restriction.labelsAllowRestriction!!.labels).containsExactlyInAnyOrder("a", "b")
assertThat(restriction.labelsExcludeRestriction).isNotNull
assertThat(restriction.labelsExcludeRestriction!!.labels).containsExactlyInAnyOrder("c", "d")
}
@Test
fun `given labels with same value in both allow and exclude when creating restriction then exclude labels are removed from allow labels`() {
val restriction = ContentRestrictions(
labelsAllow = setOf("a", "b", "B", "b ", "b", " B "),
labelsExclude = setOf(" A ", "d", "D", "d ", "d", " D ")
)
assertThat(restriction.labelsAllowRestriction).isNotNull
assertThat(restriction.labelsAllowRestriction!!.labels).containsExactlyInAnyOrder("b")
assertThat(restriction.labelsExcludeRestriction).isNotNull
assertThat(restriction.labelsExcludeRestriction!!.labels).containsExactlyInAnyOrder("a", "d")
}
@Test
fun `given allow labels with all values in exclude labels when creating restriction then allow labels is null`() {
val restriction = ContentRestrictions(
labelsAllow = setOf("a", "b", "B", "b ", "b", " B "),
labelsExclude = setOf(" A ", "b", "B", "B ", "b", " B ")
)
assertThat(restriction.labelsAllowRestriction).isNull()
assertThat(restriction.labelsExcludeRestriction).isNotNull
assertThat(restriction.labelsExcludeRestriction!!.labels).containsExactlyInAnyOrder("a", "b")
}
}

View file

@ -2,7 +2,6 @@ package org.gotson.komga.domain.model
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.domain.model.ContentRestriction.AgeRestriction
import org.gotson.komga.domain.model.ContentRestriction.LabelsRestriction
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
@ -15,59 +14,101 @@ class KomgaUserTest {
@Test
fun `given user with age AllowOnlyUnder restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(restrictions = setOf(AgeRestriction.AllowOnlyUnder(5)))
val user = defaultUser.copy(restrictions = ContentRestrictions(AgeRestriction.AllowOnlyUnder(5)))
assertThat(user.isContentRestricted(ageRating = 3)).isFalse
assertThat(user.isContentRestricted(ageRating = 5)).isFalse
assertThat(user.isContentRestricted(ageRating = 8)).isTrue
assertThat(user.isContentRestricted(ageRating = null)).isTrue
assertThat(user.isContentAllowed(ageRating = 3)).isTrue
assertThat(user.isContentAllowed(ageRating = 5)).isTrue
assertThat(user.isContentAllowed(ageRating = 8)).isFalse
assertThat(user.isContentAllowed(ageRating = null)).isFalse
}
@Test
fun `given user with age ExcludeOver restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(restrictions = setOf(AgeRestriction.ExcludeOver(16)))
val user = defaultUser.copy(restrictions = ContentRestrictions(AgeRestriction.ExcludeOver(16)))
assertThat(user.isContentRestricted(ageRating = 10)).isFalse
assertThat(user.isContentRestricted(ageRating = null)).isFalse
assertThat(user.isContentRestricted(ageRating = 16)).isTrue
assertThat(user.isContentRestricted(ageRating = 18)).isTrue
assertThat(user.isContentAllowed(ageRating = 10)).`as`("age 10 is allowed").isTrue
assertThat(user.isContentAllowed(ageRating = null)).`as`("age null is allowed").isTrue
assertThat(user.isContentAllowed(ageRating = 16)).`as`("age 16 is not allowed").isFalse
assertThat(user.isContentAllowed(ageRating = 18)).`as`("age 18 is not allowed").isFalse
}
@Test
fun `given user with sharing label AllowOnly restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(restrictions = setOf(LabelsRestriction.AllowOnly(setOf("allow", "this"))))
val user = defaultUser.copy(restrictions = ContentRestrictions(labelsAllow = setOf("allow", "this")))
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("this"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "this"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("other"))).isTrue
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("allow"))).`as`("any tag is fine: allow").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("this"))).`as`("any tag is fine: this").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("allow", "this"))).`as`("both tags are fine").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("other"))).`as`("no allowed tags: other").isFalse
assertThat(user.isContentAllowed(sharingLabels = emptySet())).`as`("no allowed tags: emptySet").isFalse
}
@Test
fun `given user with sharing label Exclude restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(restrictions = setOf(LabelsRestriction.Exclude(setOf("exclude", "this"))))
val user = defaultUser.copy(restrictions = ContentRestrictions(labelsExclude = setOf("exclude", "this")))
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("other", "this"))).isTrue
assertThat(user.isContentRestricted(sharingLabels = setOf("this"))).isTrue
assertThat(user.isContentAllowed(sharingLabels = emptySet())).`as`("no label so no exclusion").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("allow"))).`as`("label allow is not in exclusion list").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("other", "this"))).`as`("label this is in exclusion list, other label is ignored").isFalse
assertThat(user.isContentAllowed(sharingLabels = setOf("this"))).`as`("label this is in exclusion list").isFalse
}
@Test
fun `given user with both sharing label AllowOnly and Exclude restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(
restrictions = setOf(
LabelsRestriction.AllowOnly(setOf("allow", "both")),
LabelsRestriction.Exclude(setOf("exclude", "both")),
restrictions = ContentRestrictions(
labelsAllow = setOf("allow", "both"),
labelsExclude = setOf("exclude", "both"),
),
)
assertThat(user.isContentAllowed(sharingLabels = setOf("allow"))).isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("allow", "other"))).isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("allow", "both"))).isFalse
assertThat(user.isContentAllowed(sharingLabels = setOf("exclude"))).isFalse
assertThat(user.isContentAllowed(sharingLabels = emptySet())).isFalse
}
@Test
fun `given user with both age AllowOnlyUnder restriction and sharing label AllowOnly restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(
restrictions = ContentRestrictions(
ageRestriction = AgeRestriction.AllowOnlyUnder(10),
labelsAllow = setOf("allow"),
)
)
assertThat(user.isContentRestricted(sharingLabels = setOf("allow"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "other"))).isFalse
assertThat(user.isContentRestricted(sharingLabels = setOf("allow", "both"))).isTrue
assertThat(user.isContentRestricted(sharingLabels = setOf("exclude"))).isTrue
assertThat(user.isContentRestricted(sharingLabels = emptySet())).isTrue
assertThat(user.isContentAllowed(ageRating = 5)).`as`("age 5 only is sufficient").isTrue
assertThat(user.isContentAllowed(ageRating = 15)).`as`("age 15 is not allowed").isFalse
assertThat(user.isContentAllowed(ageRating = null)).`as`("missing age and no allowed label: age null").isFalse
assertThat(user.isContentAllowed(sharingLabels = setOf("allow"))).`as`("allowed tag is sufficient").isTrue
assertThat(user.isContentAllowed(sharingLabels = setOf("other"))).`as`("missing age and no allowed label: other").isFalse
assertThat(user.isContentAllowed(sharingLabels = emptySet())).`as`("missing age and empty set label").isFalse
assertThat(user.isContentAllowed(5, setOf("allow"))).`as`("age and tag are good").isTrue
assertThat(user.isContentAllowed(5, setOf("other"))).`as`("age is good, other tag is ignored").isTrue
assertThat(user.isContentAllowed(15, setOf("allow"))).`as`("age is ignored, tag is allowed").isTrue
assertThat(user.isContentAllowed(15, setOf("other"))).`as`("age is too high, and no tag is allowed").isFalse
}
@Test
fun `given user with both age AllowOnlyUnder restriction and sharing label Exclude restriction when checking for content restriction then it is accurate`() {
val user = defaultUser.copy(
restrictions = ContentRestrictions(
ageRestriction = AgeRestriction.AllowOnlyUnder(10),
labelsExclude = setOf("exclude"),
)
)
assertThat(user.isContentAllowed(ageRating = 5)).isTrue
assertThat(user.isContentAllowed(ageRating = 15)).isFalse
assertThat(user.isContentAllowed(ageRating = null)).isFalse
assertThat(user.isContentAllowed(sharingLabels = setOf("exclude"))).isFalse
assertThat(user.isContentAllowed(sharingLabels = setOf("other"))).isFalse
assertThat(user.isContentAllowed(sharingLabels = emptySet())).isFalse
assertThat(user.isContentAllowed(5, setOf("exclude"))).isFalse
assertThat(user.isContentAllowed(5, setOf("other"))).isTrue
assertThat(user.isContentAllowed(15, setOf("exclude"))).isFalse
assertThat(user.isContentAllowed(15, setOf("other"))).isFalse
}
}
}

View file

@ -130,7 +130,7 @@ class BookControllerTest(
inner class RestrictedContent {
@Test
@WithMockCustomUser(allowAgeUnder = 10)
fun `given user only allowed content with specific age rating when getting series then only gets books that satisfies this criteria`() {
fun `given user only allowed content with specific age rating when getting books then only gets books that satisfies this criteria`() {
val book10 = makeBook("book_10", libraryId = library.id)
makeSeries(name = "series_10", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
@ -188,7 +188,7 @@ class BookControllerTest(
@Test
@WithMockCustomUser(excludeAgeOver = 10)
fun `given user disallowed content with specific age rating when getting series then only gets books that satisfies this criteria`() {
fun `given user disallowed content with specific age rating when getting books then only gets books that satisfies this criteria`() {
val book10 = makeBook("book_10", libraryId = library.id)
makeSeries(name = "series_10", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
@ -243,6 +243,203 @@ class BookControllerTest(
jsonPath("$.content[1].name") { value(book5.name) }
}
}
@Test
@WithMockCustomUser(allowLabels = ["kids", "cute"])
fun `given user allowed content with specific labels when getting series then only gets books that satisfies this criteria`() {
val bookKids = makeBook("book_kids", libraryId = library.id)
makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookKids)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val bookCute = makeBook("book_cute", libraryId = library.id)
makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookCute)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val bookAdult = makeBook("book_adult", libraryId = library.id)
makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookAdult)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val book = makeBook("book", libraryId = library.id)
makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/books/${bookKids.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books/${bookCute.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books/${bookAdult.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books/${book.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(bookCute.name) }
jsonPath("$.content[1].name") { value(bookKids.name) }
}
}
@Test
@WithMockCustomUser(excludeLabels = ["kids", "cute"])
fun `given user disallowed content with specific labels when getting books then only gets books that satisfies this criteria`() {
val bookKids = makeBook("book_kids", libraryId = library.id)
makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookKids)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val bookCute = makeBook("book_cute", libraryId = library.id)
makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookCute)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val bookAdult = makeBook("book_adult", libraryId = library.id)
makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookAdult)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val book = makeBook("book", libraryId = library.id)
makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/books/${bookKids.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books/${bookCute.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books/${bookAdult.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books/${book.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(book.name) }
jsonPath("$.content[1].name") { value(bookAdult.name) }
}
}
@Test
@WithMockCustomUser(allowAgeUnder = 10, allowLabels = ["kids"], excludeLabels = ["adult", "teen"])
fun `given user allowed and disallowed content labels when getting books then only gets books that satisfies this criteria`() {
val bookKids = makeBook("book_kids", libraryId = library.id)
makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookKids)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val bookCute = makeBook("book_cute", libraryId = library.id)
makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookCute)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 5, sharingLabels = setOf("cute", "other")))
}
}
val bookAdult = makeBook("book_adult", libraryId = library.id)
makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookAdult)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val book = makeBook("book", libraryId = library.id)
makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/books/${bookKids.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books/${bookCute.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/books/${bookAdult.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books/${book.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(bookCute.name) }
jsonPath("$.content[1].name") { value(bookKids.name) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16, allowLabels = ["teen"])
fun `given user allowed and disallowed content labels when getting books then only gets books that satisfies this criteria (2)`() {
val bookTeen16 = makeBook("book_teen_16", libraryId = library.id)
makeSeries(name = "series_teen_16", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookTeen16)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen"), ageRating = 16))
}
}
mockMvc.get("/api/v1/books/${bookTeen16.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/books")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(0) }
}
}
}
@Nested

View file

@ -1,6 +1,7 @@
package org.gotson.komga.interfaces.api.rest
import org.gotson.komga.domain.model.ContentRestriction
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
@ -40,16 +41,13 @@ class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<With
rolePageStreaming = customUser.roles.contains(ROLE_PAGE_STREAMING),
sharedAllLibraries = customUser.sharedAllLibraries,
sharedLibrariesIds = customUser.sharedLibraries.toSet(),
restrictions = buildSet {
if (customUser.allowAgeUnder >= 0) add(ContentRestriction.AgeRestriction.AllowOnlyUnder(customUser.allowAgeUnder))
if (customUser.excludeAgeOver >= 0) add(ContentRestriction.AgeRestriction.ExcludeOver(customUser.excludeAgeOver))
if (customUser.allowLabels.isNotEmpty()) {
add(ContentRestriction.LabelsRestriction.AllowOnly(customUser.allowLabels.toSet()))
}
if (customUser.excludeLabels.isNotEmpty()) {
add(ContentRestriction.LabelsRestriction.Exclude(customUser.excludeLabels.toSet()))
}
},
restrictions = ContentRestrictions(
ageRestriction = if (customUser.allowAgeUnder >= 0) ContentRestriction.AgeRestriction.AllowOnlyUnder(customUser.allowAgeUnder)
else if (customUser.excludeAgeOver >= 0) ContentRestriction.AgeRestriction.ExcludeOver(customUser.excludeAgeOver)
else null,
labelsAllow = customUser.allowLabels.toSet(),
labelsExclude = customUser.excludeLabels.toSet(),
),
id = customUser.id,
),
)

View file

@ -176,9 +176,9 @@ class ReadListControllerTest(
inner class ContentRestriction {
@Test
@WithMockCustomUser(allowAgeUnder = 10)
fun `given user only allowed content with specific age rating when getting collections then only get collections that satisfies this criteria`() {
fun `given user only allowed content with specific age rating when getting read lists then only get read lists that satisfies this criteria`() {
val book10 = makeBook("book_10", libraryId = library1.id)
val series10 = makeSeries(name = "series_10", libraryId = library1.id).also { series ->
makeSeries(name = "series_10", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book10)
seriesLifecycle.addBooks(created, books)
@ -189,7 +189,7 @@ class ReadListControllerTest(
}
val book = makeBook("book", libraryId = library1.id)
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
@ -252,6 +252,391 @@ class ReadListControllerTest(
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16)
fun `given user disallowed content with specific age rating when getting read lists then only get read lists that satisfies this criteria`() {
val book10 = makeBook("book_10", libraryId = library1.id)
makeSeries(name = "series_10", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book10)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 10))
}
}
val book18 = makeBook("1", libraryId = library1.id)
makeSeries(name = "series_18", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book18)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 18))
}
}
val book16 = makeBook("1", libraryId = library1.id)
makeSeries(name = "series_16", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book16)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 16))
}
}
val book = makeBook("book", libraryId = library1.id)
makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
val rlAllowed = readListLifecycle.addReadList(
ReadList(
name = "Allowed",
bookIds = listOf(book10.id, book.id).toIndexedMap(),
),
)
val rlFiltered = readListLifecycle.addReadList(
ReadList(
name = "Filtered",
bookIds = listOf(book10.id, book.id, book16.id, book18.id).toIndexedMap(),
),
)
val rlDenied = readListLifecycle.addReadList(
ReadList(
name = "Denied",
bookIds = listOf(book16.id, book18.id).toIndexedMap(),
),
)
mockMvc.get("/api/v1/readlists")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/readlists/${rlFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/books/${book10.id}/readlists")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeLabels = ["kids", "cute"])
fun `given user disallowed content with specific labels when getting read lists then only get read lists that satisfies this criteria`() {
val bookKids = makeBook("book_kids", libraryId = library1.id)
makeSeries(name = "series_kids", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookKids)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val bookCute = makeBook("1", libraryId = library1.id)
makeSeries(name = "series_cute", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookCute)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val bookAdult = makeBook("1", libraryId = library1.id)
makeSeries(name = "series_adult", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookAdult)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val book = makeBook("book", libraryId = library1.id)
makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
val rlAllowed = readListLifecycle.addReadList(
ReadList(
name = "Allowed",
bookIds = listOf(bookAdult.id, book.id).toIndexedMap(),
),
)
val rlFiltered = readListLifecycle.addReadList(
ReadList(
name = "Filtered",
bookIds = listOf(bookKids.id, book.id, bookAdult.id, bookCute.id).toIndexedMap(),
),
)
val rlDenied = readListLifecycle.addReadList(
ReadList(
name = "Denied",
bookIds = listOf(bookKids.id, bookCute.id).toIndexedMap(),
),
)
mockMvc.get("/api/v1/readlists")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/readlists/${rlFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/books/${bookAdult.id}/readlists")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(allowAgeUnder = 10, allowLabels = ["kids"], excludeLabels = ["adult", "teen"])
fun `given user allowed and disallowed content when getting read lists then only get read lists that satisfies this criteria`() {
val bookKids = makeBook("book_kids", libraryId = library1.id)
makeSeries(name = "series_kids", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookKids)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val bookCute = makeBook("book_cute", libraryId = library1.id)
makeSeries(name = "series_cute", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookCute)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 5, sharingLabels = setOf("cute", "other")))
}
}
val bookAdult = makeBook("book_adult", libraryId = library1.id)
makeSeries(name = "series_adult", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookAdult)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val book = makeBook("book", libraryId = library1.id)
makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(book)
seriesLifecycle.addBooks(created, books)
}
}
val rlAllowed = readListLifecycle.addReadList(
ReadList(
name = "Allowed",
bookIds = listOf(bookKids.id, bookCute.id).toIndexedMap(),
),
)
val rlFiltered = readListLifecycle.addReadList(
ReadList(
name = "Filtered",
bookIds = listOf(bookKids.id, book.id, bookAdult.id, bookCute.id).toIndexedMap(),
),
)
val rlDenied = readListLifecycle.addReadList(
ReadList(
name = "Denied",
bookIds = listOf(bookAdult.id, book.id).toIndexedMap(),
),
)
mockMvc.get("/api/v1/readlists")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/readlists/${rlFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/books/${bookKids.id}/readlists")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16, allowLabels = ["teen"])
fun `given user allowed and disallowed content when getting read lists then only get read lists that satisfies this criteria (2)`() {
val bookTeen16 = makeBook("book_teen_16", libraryId = library1.id)
makeSeries(name = "series_teen_16", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookTeen16)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen"), ageRating = 16))
}
}
val bookTeen = makeBook("1", libraryId = library1.id)
makeSeries(name = "series_teen", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(bookTeen)
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen")))
}
}
val rlAllowed = readListLifecycle.addReadList(
ReadList(
name = "Allowed",
bookIds = listOf(bookTeen.id).toIndexedMap(),
),
)
val rlFiltered = readListLifecycle.addReadList(
ReadList(
name = "Filtered",
bookIds = listOf(bookTeen16.id, bookTeen.id).toIndexedMap(),
),
)
val rlDenied = readListLifecycle.addReadList(
ReadList(
name = "Denied",
bookIds = listOf(bookTeen16.id).toIndexedMap(),
),
)
mockMvc.get("/api/v1/readlists")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(1) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/readlists/${rlFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.bookIds.length()") { value(1) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/readlists/${rlDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/books/${bookTeen.id}/readlists")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${rlAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${rlFiltered.name}')].filtered") { value(true) }
}
}
}
@Nested

View file

@ -246,6 +246,474 @@ class SeriesCollectionControllerTest(
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16)
fun `given user disallowed content with specific age rating when getting collections then only gets collections that satisfies this criteria`() {
val series10 = makeSeries(name = "series_10", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 10))
}
}
val series18 = makeSeries(name = "series_18", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 18))
}
}
val series16 = makeSeries(name = "series_16", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 16))
}
}
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
}
val colAllowed = collectionLifecycle.addCollection(
SeriesCollection(
name = "Allowed",
seriesIds = listOf(series10.id, series.id),
),
)
val colFiltered = collectionLifecycle.addCollection(
SeriesCollection(
name = "Filtered",
seriesIds = listOf(series10.id, series16.id, series18.id, series.id),
),
)
val colDenied = collectionLifecycle.addCollection(
SeriesCollection(
name = "Denied",
seriesIds = listOf(series16.id, series18.id),
),
)
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/collections/${colFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/series/${series10.id}/collections")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(allowLabels = ["kids", "cute"])
fun `given user allowed only content with specific labels when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
}
val colAllowed = collectionLifecycle.addCollection(
SeriesCollection(
name = "Allowed",
seriesIds = listOf(seriesKids.id, seriesCute.id),
),
)
val colFiltered = collectionLifecycle.addCollection(
SeriesCollection(
name = "Filtered",
seriesIds = listOf(series.id, seriesKids.id, seriesCute.id, seriesAdult.id),
),
)
val colDenied = collectionLifecycle.addCollection(
SeriesCollection(
name = "Denied",
seriesIds = listOf(seriesAdult.id, series.id),
),
)
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/collections/${colFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/series/${seriesKids.id}/collections")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeLabels = ["kids", "cute"])
fun `given user disallowed content with specific labels when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
}
val colAllowed = collectionLifecycle.addCollection(
SeriesCollection(
name = "Allowed",
seriesIds = listOf(seriesAdult.id, series.id),
),
)
val colFiltered = collectionLifecycle.addCollection(
SeriesCollection(
name = "Filtered",
seriesIds = listOf(seriesAdult.id, seriesCute.id, seriesKids.id, series.id),
),
)
val colDenied = collectionLifecycle.addCollection(
SeriesCollection(
name = "Denied",
seriesIds = listOf(seriesKids.id, seriesCute.id),
),
)
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/collections/${colFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/series/${series.id}/collections")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(allowAgeUnder = 10, allowLabels = ["kids"], excludeLabels = ["adult", "teen"])
fun `given user allowed and disallowed content when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 5, sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
}
val colAllowed = collectionLifecycle.addCollection(
SeriesCollection(
name = "Allowed",
seriesIds = listOf(seriesKids.id, seriesCute.id),
),
)
val colFiltered = collectionLifecycle.addCollection(
SeriesCollection(
name = "Filtered",
seriesIds = listOf(series.id, seriesKids.id, seriesCute.id, seriesAdult.id),
),
)
val colDenied = collectionLifecycle.addCollection(
SeriesCollection(
name = "Denied",
seriesIds = listOf(seriesAdult.id, series.id),
),
)
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/collections/${colFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(2) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/series/${seriesKids.id}/collections")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16, allowLabels = ["teen"])
fun `given user allowed and disallowed content when getting series then only gets series that satisfies this criteria (2)`() {
val seriesTeen16 = makeSeries(name = "series_teen_16", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen"), ageRating = 16))
}
}
val seriesTeen = makeSeries(name = "series_teen", libraryId = library1.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library1.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen")))
}
}
val colAllowed = collectionLifecycle.addCollection(
SeriesCollection(
name = "Allowed",
seriesIds = listOf(seriesTeen.id),
),
)
val colFiltered = collectionLifecycle.addCollection(
SeriesCollection(
name = "Filtered",
seriesIds = listOf(seriesTeen16.id, seriesTeen.id),
),
)
val colDenied = collectionLifecycle.addCollection(
SeriesCollection(
name = "Denied",
seriesIds = listOf(seriesTeen16.id),
),
)
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk() }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$.content[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colAllowed.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(1) }
jsonPath("$.filtered") { value(false) }
}
mockMvc.get("/api/v1/collections/${colFiltered.id}")
.andExpect {
status { isOk() }
jsonPath("$.seriesIds.length()") { value(1) }
jsonPath("$.filtered") { value(true) }
}
mockMvc.get("/api/v1/collections/${colDenied.id}")
.andExpect {
status { isNotFound() }
}
mockMvc.get("/api/v1/series/${seriesTeen.id}/collections")
.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == '${colAllowed.name}')].filtered") { value(false) }
jsonPath("$[?(@.name == '${colFiltered.name}')].filtered") { value(true) }
}
}
}
@Nested

View file

@ -352,6 +352,190 @@ class SeriesControllerTest(
jsonPath("$.content[1].name") { value("series_no") }
}
}
@Test
@WithMockCustomUser(allowLabels = ["kids", "cute"])
fun `given user allowed only content with specific labels when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/series/${seriesKids.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series/${seriesCute.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series/${seriesAdult.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series/${series.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(seriesCute.name) }
jsonPath("$.content[1].name") { value(seriesKids.name) }
}
}
@Test
@WithMockCustomUser(excludeLabels = ["kids", "cute"])
fun `given user disallowed content with specific labels when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/series/${seriesKids.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series/${seriesCute.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series/${seriesAdult.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series/${series.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(seriesAdult.name) }
jsonPath("$.content[1].name") { value(series.name) }
}
}
@Test
@WithMockCustomUser(allowAgeUnder = 10, allowLabels = ["kids"], excludeLabels = ["adult", "teen"])
fun `given user allowed and disallowed content when getting series then only gets series that satisfies this criteria`() {
val seriesKids = makeSeries(name = "series_kids", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("kids")))
}
}
val seriesCute = makeSeries(name = "series_cute", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(ageRating = 5, sharingLabels = setOf("cute", "other")))
}
}
val seriesAdult = makeSeries(name = "series_adult", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("adult")))
}
}
val series = makeSeries(name = "series_no", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
}
mockMvc.get("/api/v1/series/${seriesKids.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series/${seriesCute.id}").andExpect { status { isOk() } }
mockMvc.get("/api/v1/series/${seriesAdult.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series/${series.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(2) }
jsonPath("$.content[0].name") { value(seriesCute.name) }
jsonPath("$.content[1].name") { value(seriesKids.name) }
}
}
@Test
@WithMockCustomUser(excludeAgeOver = 16, allowLabels = ["teen"])
fun `given user allowed and disallowed content when getting series then only gets series that satisfies this criteria (2)`() {
val seriesTeen16 = makeSeries(name = "series_teen_16", libraryId = library.id).also { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
seriesMetadataRepository.findById(series.id).let {
seriesMetadataRepository.update(it.copy(sharingLabels = setOf("teen"), ageRating = 16))
}
}
mockMvc.get("/api/v1/series/${seriesTeen16.id}").andExpect { status { isForbidden() } }
mockMvc.get("/api/v1/series")
.andExpect {
status { isOk() }
jsonPath("$.content.length()") { value(0) }
}
}
}
@Nested