mirror of
https://github.com/gotson/komga.git
synced 2026-01-03 22:36:07 +01:00
feat: display komga.org website announcements within the app
Closes: #1149
This commit is contained in:
parent
223aea531d
commit
72c1e8dd29
17 changed files with 456 additions and 4 deletions
|
|
@ -21,6 +21,11 @@
|
|||
"account_settings": "Account Settings",
|
||||
"change_password": "change password"
|
||||
},
|
||||
"announcements": {
|
||||
"mark_all_read": "Mark all as read",
|
||||
"mark_read": "Mark as read",
|
||||
"tab_title": "Announcements"
|
||||
},
|
||||
"authentication_activity": {
|
||||
"datetime": "Date Time",
|
||||
"email": "Email",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import komgaLogin from './plugins/komga-login.plugin'
|
|||
import komgaPageHashes from './plugins/komga-pagehashes.plugin'
|
||||
import komgaMetrics from './plugins/komga-metrics.plugin'
|
||||
import komgaHistory from './plugins/komga-history.plugin'
|
||||
import komgaAnnouncements from './plugins/komga-announcements.plugin'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import logger from './plugins/logger.plugin'
|
||||
import './public-path'
|
||||
|
|
@ -72,7 +73,7 @@ Vue.use(komgaLogin, {http: Vue.prototype.$http})
|
|||
Vue.use(komgaPageHashes, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaMetrics, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaHistory, {http: Vue.prototype.$http})
|
||||
|
||||
Vue.use(komgaAnnouncements, {http: Vue.prototype.$http})
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
|
|
|||
17
komga-webui/src/plugins/komga-announcements.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-announcements.plugin.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import _Vue from 'vue'
|
||||
import KomgaAnnouncementsService from '@/services/komga-announcements.service'
|
||||
|
||||
export default {
|
||||
install(
|
||||
Vue: typeof _Vue,
|
||||
{http}: { http: AxiosInstance }) {
|
||||
Vue.prototype.$komgaAnnouncements = new KomgaAnnouncementsService(http)
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$komgaAnnouncements: KomgaAnnouncementsService;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,6 +94,12 @@ const router = new Router({
|
|||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "metrics" */ './views/Metrics.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/announcements',
|
||||
name: 'announcements',
|
||||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "announcements" */ './views/AnnouncementsView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
36
komga-webui/src/services/komga-announcements.service.ts
Normal file
36
komga-webui/src/services/komga-announcements.service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import {JsonFeedDto} from '@/types/json-feed'
|
||||
|
||||
const API_ANNOUNCEMENTS = '/api/v1/announcements'
|
||||
|
||||
export default class KomgaAnnouncementsService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor(http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async getAnnouncements(): Promise<JsonFeedDto> {
|
||||
try {
|
||||
return (await this.http.get(API_ANNOUNCEMENTS)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve announcements'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async markAnnouncementsRead(announcementIds: string[]) {
|
||||
try {
|
||||
await this.http.put(API_ANNOUNCEMENTS, announcementIds)
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to mark announcements as read'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import createPersistedState from 'vuex-persistedstate'
|
|||
import {persistedModule} from './plugins/persisted-state'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import {ReadListDto} from '@/types/komga-readlists'
|
||||
import {ItemDto, JsonFeedDto} from '@/types/json-feed'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
|
|
@ -50,6 +51,15 @@ export default new Vuex.Store({
|
|||
deleteSeriesDialog: false,
|
||||
|
||||
booksToCheck: 0,
|
||||
|
||||
announcements: {} as JsonFeedDto,
|
||||
},
|
||||
getters: {
|
||||
getUnreadAnnouncementsCount: (state) => (): number => {
|
||||
return state.announcements?.items
|
||||
.filter((value: ItemDto) => false == value._komga?.read)
|
||||
.length || 0
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
// Collections
|
||||
|
|
@ -139,6 +149,9 @@ export default new Vuex.Store({
|
|||
setDeleteSeriesDialog(state, dialog) {
|
||||
state.deleteSeriesDialog = dialog
|
||||
},
|
||||
setAnnouncements(state, announcements) {
|
||||
state.announcements = announcements
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// collections
|
||||
|
|
|
|||
28
komga-webui/src/types/json-feed.ts
Normal file
28
komga-webui/src/types/json-feed.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export interface JsonFeedDto {
|
||||
version: string,
|
||||
title: string,
|
||||
home_page_url?: string,
|
||||
description?: string,
|
||||
items: ItemDto[],
|
||||
}
|
||||
|
||||
export interface ItemDto {
|
||||
id: string,
|
||||
url?: string,
|
||||
title?: string,
|
||||
summary?: string,
|
||||
content_html?: string,
|
||||
date_modified?: Date,
|
||||
author: AuthorDto,
|
||||
tags: Set<string>,
|
||||
_komga?: KomgaExtensionDto,
|
||||
}
|
||||
|
||||
export interface AuthorDto {
|
||||
name?: string,
|
||||
url?: string,
|
||||
}
|
||||
|
||||
export interface KomgaExtensionDto {
|
||||
read: boolean,
|
||||
}
|
||||
99
komga-webui/src/views/AnnouncementsView.vue
Normal file
99
komga-webui/src/views/AnnouncementsView.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<div v-for="(item, index) in $store.state.announcements.items" :key="item.id">
|
||||
<v-row justify="space-between" align="center">
|
||||
<v-col cols="auto">
|
||||
<div class="ml-n2">
|
||||
<v-badge
|
||||
dot
|
||||
inline
|
||||
left
|
||||
:value="item._komga.read ? 0 : 1"
|
||||
color="warning"
|
||||
>
|
||||
<a :href="item.url" target="_blank" class="text-h3 font-weight-medium link-underline">{{ item.title }}</a>
|
||||
<v-icon
|
||||
x-small
|
||||
color="grey"
|
||||
class="ps-1"
|
||||
>
|
||||
mdi-open-in-new
|
||||
</v-icon>
|
||||
</v-badge>
|
||||
</div>
|
||||
<div class="mt-2 subtitle-1">
|
||||
{{ new Intl.DateTimeFormat($i18n.locale, {dateStyle: 'long'}).format(item.date_modified) }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-tooltip :left="!$vuetify.rtl" :right="$vuetify.rtl">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon elevation="5" color="success" v-on="on" :disabled="item._komga.read"
|
||||
@click="markRead(item.id)">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
{{ $t('announcements.mark_read') }}
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div v-html="item.content_html"></div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-8" v-if="index != $store.state.announcements.items.length - 1"/>
|
||||
</div>
|
||||
|
||||
<v-tooltip :left="!$vuetify.rtl" :right="$vuetify.rtl">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-fab-transition>
|
||||
<v-btn v-if="$store.getters.getUnreadAnnouncementsCount()"
|
||||
color="success"
|
||||
fab
|
||||
bottom
|
||||
right
|
||||
fixed
|
||||
elevation="10"
|
||||
v-on="on"
|
||||
@click="markAllRead"
|
||||
>
|
||||
<v-icon>mdi-check-all</v-icon>
|
||||
</v-btn>
|
||||
</v-fab-transition>
|
||||
</template>
|
||||
{{ $t('announcements.mark_all_read') }}
|
||||
</v-tooltip>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {ItemDto} from '@/types/json-feed'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AnnouncementsView',
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.$komgaAnnouncements.getAnnouncements()
|
||||
.then(x => this.$store.commit('setAnnouncements', x))
|
||||
},
|
||||
markRead(announcementId: string) {
|
||||
this.$komgaAnnouncements.markAnnouncementsRead([announcementId])
|
||||
.then(() => this.loadData())
|
||||
},
|
||||
markAllRead() {
|
||||
this.$komgaAnnouncements.markAnnouncementsRead(this.$store.state.announcements.items.map((x: ItemDto) => x.id))
|
||||
.then(() => this.loadData())
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
dot
|
||||
offset-x="15"
|
||||
offset-y="20"
|
||||
:value="drawerVisible ? 0 : booksToCheck"
|
||||
color="accent"
|
||||
:value="drawerVisible ? 0 : booksToCheck + $store.getters.getUnreadAnnouncementsCount()"
|
||||
:color="booksToCheck ? 'accent' : 'warning'"
|
||||
class="ms-n3"
|
||||
>
|
||||
<v-app-bar-nav-icon @click.stop="toggleDrawer"/>
|
||||
|
|
@ -135,7 +135,14 @@
|
|||
<v-icon>mdi-cog</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t('server_settings.server_settings') }}</v-list-item-title>
|
||||
<v-badge
|
||||
dot
|
||||
inline
|
||||
:value="$store.getters.getUnreadAnnouncementsCount()"
|
||||
color="warning"
|
||||
>
|
||||
<v-list-item-title>{{ $t('server_settings.server_settings') }}</v-list-item-title>
|
||||
</v-badge>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
|
|
@ -238,6 +245,8 @@ export default Vue.extend({
|
|||
this.info = await this.$actuator.getInfo()
|
||||
this.$komgaBooks.getBooks(undefined, {size: 0} as PageRequest, undefined, [MediaStatus.ERROR, MediaStatus.UNSUPPORTED])
|
||||
.then(x => this.$store.commit('setBooksToCheck', x.totalElements))
|
||||
this.$komgaAnnouncements.getAnnouncements()
|
||||
.then(x => this.$store.commit('setAnnouncements', x))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@
|
|||
<v-tab :to="{name: 'settings-users'}">{{ $t('users.users') }}</v-tab>
|
||||
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
|
||||
<v-tab :to="{name: 'metrics'}">{{ $t('metrics.title') }}</v-tab>
|
||||
<v-tab :to="{name: 'announcements'}">
|
||||
<v-badge
|
||||
dot
|
||||
:value="$store.getters.getUnreadAnnouncementsCount()"
|
||||
color="warning"
|
||||
>{{ $t('announcements.tab_title') }}</v-badge>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<router-view/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ dependencies {
|
|||
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.1"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
create table ANNOUNCEMENTS_READ
|
||||
(
|
||||
USER_ID varchar NOT NULL,
|
||||
ANNOUNCEMENT_ID varchar NOT NULL,
|
||||
PRIMARY KEY (USER_ID, ANNOUNCEMENT_ID),
|
||||
FOREIGN KEY (USER_ID) references USER (ID)
|
||||
);
|
||||
|
|
@ -17,4 +17,7 @@ interface KomgaUserRepository {
|
|||
|
||||
fun delete(userId: String)
|
||||
fun deleteAll()
|
||||
|
||||
fun findAnnouncementIdsReadByUserId(userId: String): Set<String>
|
||||
fun saveAnnouncementIdsRead(user: KomgaUser, announcementIds: Set<String>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.gotson.komga.domain.model.ContentRestrictions
|
|||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.persistence.KomgaUserRepository
|
||||
import org.gotson.komga.jooq.Tables
|
||||
import org.gotson.komga.jooq.tables.records.AnnouncementsReadRecord
|
||||
import org.jooq.DSLContext
|
||||
import org.jooq.Record
|
||||
import org.jooq.ResultQuery
|
||||
|
|
@ -22,6 +23,7 @@ class KomgaUserDao(
|
|||
private val u = Tables.USER
|
||||
private val ul = Tables.USER_LIBRARY_SHARING
|
||||
private val us = Tables.USER_SHARING
|
||||
private val ar = Tables.ANNOUNCEMENTS_READ
|
||||
|
||||
override fun count(): Long = dsl.fetchCount(u).toLong()
|
||||
|
||||
|
|
@ -128,6 +130,10 @@ class KomgaUserDao(
|
|||
insertSharingRestrictions(user)
|
||||
}
|
||||
|
||||
override fun saveAnnouncementIdsRead(user: KomgaUser, announcementIds: Set<String>) {
|
||||
dsl.batchStore(announcementIds.map { AnnouncementsReadRecord(user.id, it) }).execute()
|
||||
}
|
||||
|
||||
private fun insertSharedLibraries(user: KomgaUser) {
|
||||
user.sharedLibrariesIds.forEach {
|
||||
dsl.insertInto(ul)
|
||||
|
|
@ -155,6 +161,7 @@ class KomgaUserDao(
|
|||
|
||||
@Transactional
|
||||
override fun delete(userId: String) {
|
||||
dsl.deleteFrom(ar).where(ar.USER_ID.equal(userId)).execute()
|
||||
dsl.deleteFrom(us).where(us.USER_ID.equal(userId)).execute()
|
||||
dsl.deleteFrom(ul).where(ul.USER_ID.equal(userId)).execute()
|
||||
dsl.deleteFrom(u).where(u.ID.equal(userId)).execute()
|
||||
|
|
@ -162,11 +169,18 @@ class KomgaUserDao(
|
|||
|
||||
@Transactional
|
||||
override fun deleteAll() {
|
||||
dsl.deleteFrom(ar).execute()
|
||||
dsl.deleteFrom(us).execute()
|
||||
dsl.deleteFrom(ul).execute()
|
||||
dsl.deleteFrom(u).execute()
|
||||
}
|
||||
|
||||
override fun findAnnouncementIdsReadByUserId(userId: String): Set<String> =
|
||||
dsl.select(ar.ANNOUNCEMENT_ID)
|
||||
.from(ar)
|
||||
.where(ar.USER_ID.eq(userId))
|
||||
.fetchSet(ar.ANNOUNCEMENT_ID)
|
||||
|
||||
override fun existsByEmailIgnoreCase(email: String): Boolean =
|
||||
dsl.fetchExists(
|
||||
dsl.selectFrom(u)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.persistence.KomgaUserRepository
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.interfaces.api.rest.dto.JsonFeedDto
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val website = "https://komga.org"
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/announcements", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class AnnouncementController(
|
||||
private val userRepository: KomgaUserRepository,
|
||||
) {
|
||||
|
||||
private val webClient = WebClient.create("$website/blog/feed.json")
|
||||
|
||||
private val cache = Caffeine.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.DAYS)
|
||||
.build<String, JsonFeedDto>()
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
fun getAnnouncements(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
): JsonFeedDto {
|
||||
return cache.get("announcements") { fetchWebsiteAnnouncements()?.let { replaceLinks(it) } }
|
||||
?.let { feed ->
|
||||
val read = userRepository.findAnnouncementIdsReadByUserId(principal.user.id)
|
||||
feed.copy(items = feed.items.map { item -> item.copy(komgaExtension = JsonFeedDto.KomgaExtensionDto(read.contains(item.id))) })
|
||||
}
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@PutMapping
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun markAnnouncementsRead(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestBody announcementIds: Set<String>,
|
||||
) {
|
||||
userRepository.saveAnnouncementIdsRead(principal.user, announcementIds)
|
||||
}
|
||||
|
||||
fun fetchWebsiteAnnouncements(): JsonFeedDto? {
|
||||
val response = webClient.get()
|
||||
.retrieve()
|
||||
.toEntity(JsonFeedDto::class.java)
|
||||
.block()
|
||||
return response?.body
|
||||
}
|
||||
|
||||
// while waiting for https://github.com/facebook/docusaurus/issues/9136
|
||||
fun replaceLinks(feed: JsonFeedDto): JsonFeedDto = feed.copy(
|
||||
items = feed.items.map {
|
||||
it.copy(
|
||||
contentHtml =
|
||||
it.contentHtml?.replace("""<a href="/""", """<a href="$website/"""),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class JsonFeedDto(
|
||||
val version: String,
|
||||
val title: String,
|
||||
@field:JsonProperty("home_page_url")
|
||||
val homePageUrl: String?,
|
||||
val description: String?,
|
||||
val items: List<ItemDto> = emptyList(),
|
||||
) {
|
||||
data class ItemDto(
|
||||
val id: String,
|
||||
val url: String?,
|
||||
val title: String?,
|
||||
val summary: String?,
|
||||
@field:JsonProperty("content_html")
|
||||
val contentHtml: String?,
|
||||
@field:JsonProperty("date_modified")
|
||||
val dateModified: OffsetDateTime?,
|
||||
val author: AuthorDto?,
|
||||
val tags: Set<String> = emptySet(),
|
||||
@field:JsonProperty("_komga")
|
||||
val komgaExtension: KomgaExtensionDto?,
|
||||
)
|
||||
|
||||
data class AuthorDto(
|
||||
val name: String?,
|
||||
val url: String?,
|
||||
)
|
||||
|
||||
data class KomgaExtensionDto(
|
||||
val read: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.interfaces.api.rest.dto.JsonFeedDto
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc(printOnlyOnFailure = false)
|
||||
class AnnouncementControllerTest(
|
||||
@Autowired private val mockMvc: MockMvc,
|
||||
) {
|
||||
|
||||
@SpykBean
|
||||
private lateinit var announcementController: AnnouncementController
|
||||
|
||||
private val mockFeed = JsonFeedDto(
|
||||
"https://jsonfeed.org/version/1",
|
||||
"Announcements",
|
||||
"https://komga.org/blog",
|
||||
"Latest Komga announcements",
|
||||
listOf(
|
||||
JsonFeedDto.ItemDto(
|
||||
"https://komga.org/blog/prepare-v1",
|
||||
"https://komga.org/blog/prepare-v1",
|
||||
"Prepare for v1.0.0",
|
||||
"The future v1.0.0 will bring some breaking changes, this guide will help you to prepare for the next major version.",
|
||||
"""
|
||||
You can still change the port <a href="/docs/installation/configuration#server_port--serverport-port">through configuration</a>
|
||||
Another link <a href="/blog/post/">here</a>.
|
||||
A normal <a href="https://google.com">link</a>.
|
||||
""".trimIndent(),
|
||||
OffsetDateTime.of(
|
||||
LocalDate.of(2023, 3, 21).atStartOfDay(),
|
||||
ZoneOffset.UTC,
|
||||
),
|
||||
JsonFeedDto.AuthorDto("gotson", "https://github.com/gotson"),
|
||||
setOf(
|
||||
"breaking change",
|
||||
"upgrade",
|
||||
"komga",
|
||||
),
|
||||
null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(roles = [ROLE_ADMIN])
|
||||
fun `when getting announcements multiple times then the server announcements are only fetched once`() {
|
||||
every { announcementController.fetchWebsiteAnnouncements() } returns mockFeed
|
||||
|
||||
repeat(2) {
|
||||
mockMvc.get("/api/v1/announcements")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.items.length()") { value(1) }
|
||||
jsonPath("$.items[0].date_modified") { value("2023-03-21T00:00:00Z") }
|
||||
}
|
||||
}
|
||||
|
||||
verify(exactly = 1) { announcementController.fetchWebsiteAnnouncements() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given json feed with relative links when replacing then links are replaced`() {
|
||||
val feed = announcementController.replaceLinks(mockFeed)
|
||||
|
||||
assertThat(feed.items.first().contentHtml)
|
||||
.contains(
|
||||
"""<a href="https://komga.org/docs/installation/""",
|
||||
"""<a href="https://komga.org/blog/post/">here</a>""",
|
||||
"""<a href="https://google.com">link</a>""",
|
||||
)
|
||||
.doesNotContain(
|
||||
"""<a href="/docs/installation/""",
|
||||
"""<a href="/blog/post/>here</a>""",
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue