diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ContentRestrictions.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ContentRestrictions.kt new file mode 100644 index 000000000..478b3b873 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ContentRestrictions.kt @@ -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 = emptySet(), + labelsExclude: Set = 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)" +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt index c9d5a2d4f..a5c6a2489 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt @@ -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 = emptySet(), val sharedAllLibraries: Boolean = true, - val restrictions: Set = 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 = 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 = 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 { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRepository.kt index 92d5e29d1..f1b25e1cf 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRepository.kt @@ -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? = null, restrictions: Set = emptySet()): ReadList? + fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection? = 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? = null, filterOnLibraryIds: Collection? = null, search: String? = null, pageable: Pageable, restrictions: Set = emptySet()): Page + fun findAll(belongsToLibraryIds: Collection? = null, filterOnLibraryIds: Collection? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page /** * 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?, restrictions: Set = emptySet()): Collection + fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection?, restrictions: ContentRestrictions = ContentRestrictions()): Collection fun findAllEmpty(): Collection diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt index 819bcaca3..c40a3482f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt @@ -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? = null, restrictions: Set = emptySet()): SeriesCollection? + fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection? = 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? = null, filterOnLibraryIds: Collection? = null, search: String? = null, pageable: Pageable, restrictions: Set = emptySet()): Page + fun findAll(belongsToLibraryIds: Collection? = null, filterOnLibraryIds: Collection? = null, search: String? = null, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page /** * 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?, restrictions: Set = emptySet()): Collection + fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection?, restrictions: ContentRestrictions = ContentRestrictions()): Collection fun findAllEmpty(): Collection diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt index 72fbf17f4..73ab87d6d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt @@ -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): Page { - val conditions = search.toCondition().and(restrictions.toCondition()) + override fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions): Page { + 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?, pageable: Pageable, restrictions: Set): Page { + override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection?, pageable: Pageable, restrictions: ContentRestrictions): Page { 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()))) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt index 46005de5f..618a397d0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt @@ -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?, restrictions: Set): ReadList? = - selectBase(restrictions.isNotEmpty()) + override fun findByIdOrNull(readListId: String, filterOnLibraryIds: Collection?, 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?, filterOnLibraryIds: Collection?, search: String?, pageable: Pageable, restrictions: Set): Page { + override fun findAll(belongsToLibraryIds: Collection?, filterOnLibraryIds: Collection?, search: String?, pageable: Pageable, restrictions: ContentRestrictions): Page { 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?, restrictions: Set): Collection { + override fun findAllContainingBookId(containsBookId: String, filterOnLibraryIds: Collection?, restrictions: ContentRestrictions): Collection { 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.fetchAndMap(filterOnLibraryIds: Collection?, restrictions: Set = emptySet()): List = + private fun ResultQuery.fetchAndMap(filterOnLibraryIds: Collection?, restrictions: ContentRestrictions = ContentRestrictions()): List = 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 } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt index 4d97af5b4..1271c7462 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt @@ -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?, restrictions: Set): SeriesCollection? = - selectBase(restrictions.isNotEmpty()) + override fun findByIdOrNull(collectionId: String, filterOnLibraryIds: Collection?, 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?, filterOnLibraryIds: Collection?, search: String?, pageable: Pageable, restrictions: Set): Page { + override fun findAll(belongsToLibraryIds: Collection?, filterOnLibraryIds: Collection?, search: String?, pageable: Pageable, restrictions: ContentRestrictions): Page { 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?, restrictions: Set): Collection { + override fun findAllContainingSeriesId(containsSeriesId: String, filterOnLibraryIds: Collection?, restrictions: ContentRestrictions): Collection { 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.fetchAndMap(filterOnLibraryIds: Collection?, restrictions: Set = emptySet()): List = + private fun ResultQuery.fetchAndMap(filterOnLibraryIds: Collection?, restrictions: ContentRestrictions = ContentRestrictions()): List = 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 } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt index 516aff7fa..554bbd3c1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt @@ -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): Page { - val conditions = search.toCondition().and(restrictions.toCondition()) + override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions): Page { + 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, + restrictions: ContentRestrictions, ): Page { - 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, + restrictions: ContentRestrictions, pageable: Pageable, ): Page { 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): List { - val conditions = search.toCondition().and(restrictions.toCondition()) + override fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions): List { + 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 { diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt index 88c0dbfe4..ef491894b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt @@ -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) fun DSLContext.selectTempStrings() = this.select(Tables.TEMP_STRING_LIST.STRING).from(Tables.TEMP_STRING_LIST) -fun Set.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)) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/Utils.kt index 2d418fea6..b3e60c2b6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/Utils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/Utils.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt index e37f1bfe7..5e02dc4e5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt @@ -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 = emptySet()): Page + fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page /** * Find books that are part of a readlist, optionally filtered by library @@ -39,7 +39,7 @@ interface BookDtoRepository { filterOnLibraryIds: Collection?, ): BookDto? - fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection?, pageable: Pageable, restrictions: Set = emptySet()): Page + fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection?, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page fun findAllDuplicates(userId: String, pageable: Pageable): Page } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/SeriesDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/SeriesDtoRepository.kt index 5de247b78..87a4036cd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/SeriesDtoRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/SeriesDtoRepository.kt @@ -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 = emptySet()): Page - fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: Set = emptySet()): Page - fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set, pageable: Pageable): Page + fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page + fun findAllByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable, restrictions: ContentRestrictions = ContentRestrictions()): Page + fun findAllRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions, pageable: Pageable): Page - fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: Set): List + fun countByFirstCharacter(search: SeriesSearchWithReadProgress, userId: String, restrictions: ContentRestrictions): List } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index 0cf3a891f..5bafdfd91 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt index 1a026926a..06ac5360b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt @@ -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) } } } diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/model/ContentRestrictionsTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/model/ContentRestrictionsTest.kt new file mode 100644 index 000000000..aea2d354e --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/domain/model/ContentRestrictionsTest.kt @@ -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") + } +} diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/model/KomgaUserTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/model/KomgaUserTest.kt index 1e9dfb906..95bb40714 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/model/KomgaUserTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/model/KomgaUserTest.kt @@ -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 } } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt index 1db6e361f..54172d7bd 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt @@ -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 diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/MockSpringSecurity.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/MockSpringSecurity.kt index 4a1813466..71c7ab0b4 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/MockSpringSecurity.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/MockSpringSecurity.kt @@ -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= 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, ), ) diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt index 8d88d4144..320ff6de5 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt @@ -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 diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionControllerTest.kt index 5d57c952b..876764c1d 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionControllerTest.kt @@ -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 diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt index c4b5873a0..b3da3df6f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt @@ -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