diff --git a/README.md b/README.md index 31b460d24..f21bc7bd9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台 - 集成 OpenAI 提供的 Markdown 格式化功能 - 通过环境变量可调整密码强度、登录方式、保护码等多种配置 - 支持图片上传,默认使用腾讯云 COS 扩展 +- 浏览器推送通知,离开网站也能及时收到提醒 ## 🌟 项目优势 - 全面开源,便于二次开发和自定义扩展 @@ -39,6 +40,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台 - 模块化设计,代码结构清晰,维护成本低 - REST API 可接入任意前端框架,兼容多端平台 - 配置简单,通过环境变量快速调整和部署 +- 如需推送通知,请设置 `WEBPUSH_PUBLIC_KEY` 和 `WEBPUSH_PRIVATE_KEY` 环境变量 ## 🏘️ 社区 diff --git a/open-isle-cli/public/notifications-sw.js b/open-isle-cli/public/notifications-sw.js new file mode 100644 index 000000000..1d8aacc13 --- /dev/null +++ b/open-isle-cli/public/notifications-sw.js @@ -0,0 +1,23 @@ +self.addEventListener('push', function(event) { + let payload = { body: 'New notification', url: '/' } + try { + if (event.data) payload = JSON.parse(event.data.text()) + } catch (e) { + if (event.data) payload.body = event.data.text() + } + event.waitUntil( + self.registration.showNotification('OpenIsle', { + body: payload.body, + icon: '/favicon.ico', + data: { url: payload.url } + }) + ) +}) + +self.addEventListener('notificationclick', function(event) { + const url = event.notification.data && event.notification.data.url + event.notification.close() + if (url) { + event.waitUntil(clients.openWindow(url)) + } +}) diff --git a/open-isle-cli/src/utils/discord.js b/open-isle-cli/src/utils/discord.js index 4fa45ac84..521ecc8fa 100644 --- a/open-isle-cli/src/utils/discord.js +++ b/open-isle-cli/src/utils/discord.js @@ -1,6 +1,7 @@ import { API_BASE_URL, DISCORD_CLIENT_ID, toast } from '../main' import { WEBSITE_BASE_URL } from '../constants' import { setToken, loadCurrentUser } from './auth' +import { registerPush } from './push' export function discordAuthorize(state = '') { if (!DISCORD_CLIENT_ID) { @@ -24,6 +25,7 @@ export async function discordExchange(code, state, reason) { setToken(data.token) await loadCurrentUser() toast.success('登录成功') + registerPush() return { success: true, needReason: false diff --git a/open-isle-cli/src/utils/github.js b/open-isle-cli/src/utils/github.js index 4b0cb6f68..cca908654 100644 --- a/open-isle-cli/src/utils/github.js +++ b/open-isle-cli/src/utils/github.js @@ -1,6 +1,7 @@ import { API_BASE_URL, GITHUB_CLIENT_ID, toast } from '../main' import { setToken, loadCurrentUser } from './auth' import { WEBSITE_BASE_URL } from '../constants' +import { registerPush } from './push' export function githubAuthorize(state = '') { if (!GITHUB_CLIENT_ID) { @@ -24,6 +25,7 @@ export async function githubExchange(code, state, reason) { setToken(data.token) await loadCurrentUser() toast.success('登录成功') + registerPush() return { success: true, needReason: false diff --git a/open-isle-cli/src/utils/google.js b/open-isle-cli/src/utils/google.js index fcdf0c92b..533d6c808 100644 --- a/open-isle-cli/src/utils/google.js +++ b/open-isle-cli/src/utils/google.js @@ -1,5 +1,6 @@ import { API_BASE_URL, GOOGLE_CLIENT_ID, toast } from '../main' import { setToken, loadCurrentUser } from './auth' +import { registerPush } from './push' export async function googleGetIdToken() { return new Promise((resolve, reject) => { @@ -29,6 +30,7 @@ export async function googleAuthWithToken(idToken, redirect_success, redirect_no setToken(data.token) await loadCurrentUser() toast.success('登录成功') + registerPush() if (redirect_success) redirect_success() } else if (data.reason_code === 'NOT_APPROVED') { toast.info('当前为注册审核模式,请填写注册理由') diff --git a/open-isle-cli/src/utils/push.js b/open-isle-cli/src/utils/push.js new file mode 100644 index 000000000..201b8cc60 --- /dev/null +++ b/open-isle-cli/src/utils/push.js @@ -0,0 +1,48 @@ +import { API_BASE_URL } from '../main' +import { getToken } from './auth' + +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4) + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') + const rawData = atob(base64) + const outputArray = new Uint8Array(rawData.length) + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i) + } + return outputArray +} + +function arrayBufferToBase64(buffer) { + const bytes = new Uint8Array(buffer) + let binary = '' + for (const b of bytes) binary += String.fromCharCode(b) + return btoa(binary) +} + +export async function registerPush() { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) return + try { + const reg = await navigator.serviceWorker.register('/notifications-sw.js') + const res = await fetch(`${API_BASE_URL}/api/push/public-key`) + if (!res.ok) return + const { key } = await res.json() + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(key) + }) + await fetch(`${API_BASE_URL}/api/push/subscribe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getToken()}` + }, + body: JSON.stringify({ + endpoint: sub.endpoint, + p256dh: arrayBufferToBase64(sub.getKey('p256dh')), + auth: arrayBufferToBase64(sub.getKey('auth')) + }) + }) + } catch (e) { + // ignore + } +} diff --git a/open-isle-cli/src/utils/twitter.js b/open-isle-cli/src/utils/twitter.js index cc19da4db..8362d17af 100644 --- a/open-isle-cli/src/utils/twitter.js +++ b/open-isle-cli/src/utils/twitter.js @@ -1,6 +1,7 @@ import { API_BASE_URL, TWITTER_CLIENT_ID, toast } from '../main' import { WEBSITE_BASE_URL } from '../constants' import { setToken, loadCurrentUser } from './auth' +import { registerPush } from './push' function generateCodeVerifier() { const array = new Uint8Array(32) @@ -59,6 +60,7 @@ export async function twitterExchange(code, state, reason) { setToken(data.token) await loadCurrentUser() toast.success('登录成功') + registerPush() return { success: true, needReason: false } } else if (data.reason_code === 'NOT_APPROVED') { toast.info('当前为注册审核模式,请填写注册理由') diff --git a/open-isle-cli/src/views/LoginPageView.vue b/open-isle-cli/src/views/LoginPageView.vue index c8418ca9d..cba93fb48 100644 --- a/open-isle-cli/src/views/LoginPageView.vue +++ b/open-isle-cli/src/views/LoginPageView.vue @@ -59,6 +59,7 @@ import { githubAuthorize } from '../utils/github' import { discordAuthorize } from '../utils/discord' import { twitterAuthorize } from '../utils/twitter' import BaseInput from '../components/BaseInput.vue' +import { registerPush } from '../utils/push' export default { name: 'LoginPageView', components: { BaseInput }, @@ -87,6 +88,7 @@ export default { setToken(data.token) await loadCurrentUser() toast.success('登录成功') + registerPush() this.$router.push('/') } else if (data.reason_code === 'NOT_VERIFIED') { toast.info('当前邮箱未验证,已经为您重新发送验证码') diff --git a/pom.xml b/pom.xml index cc82b1772..445fa4058 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,16 @@ spring-boot-starter-test test + + nl.martijndwars + web-push + 5.1.1 + + + org.bouncycastle + bcprov-jdk15on + 1.70 + diff --git a/src/main/java/com/openisle/config/SecurityConfig.java b/src/main/java/com/openisle/config/SecurityConfig.java index 44ef64d65..e93419ca2 100644 --- a/src/main/java/com/openisle/config/SecurityConfig.java +++ b/src/main/java/com/openisle/config/SecurityConfig.java @@ -108,6 +108,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll() .requestMatchers(HttpMethod.GET, "/api/search/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/users/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll() .requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll() .requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN") @@ -140,7 +141,7 @@ public class SecurityConfig { uri.startsWith("/api/categories") || uri.startsWith("/api/tags") || uri.startsWith("/api/search") || uri.startsWith("/api/users") || uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") || - uri.startsWith("/api/activities")); + uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key")); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); diff --git a/src/main/java/com/openisle/controller/PushSubscriptionController.java b/src/main/java/com/openisle/controller/PushSubscriptionController.java new file mode 100644 index 000000000..800287b40 --- /dev/null +++ b/src/main/java/com/openisle/controller/PushSubscriptionController.java @@ -0,0 +1,41 @@ +package com.openisle.controller; + +import com.openisle.service.PushSubscriptionService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/push") +@RequiredArgsConstructor +public class PushSubscriptionController { + private final PushSubscriptionService pushSubscriptionService; + @Value("${app.webpush.public-key}") + private String publicKey; + + @GetMapping("/public-key") + public PublicKeyResponse getPublicKey() { + PublicKeyResponse r = new PublicKeyResponse(); + r.setKey(publicKey); + return r; + } + + @PostMapping("/subscribe") + public void subscribe(@RequestBody SubscriptionRequest req, Authentication auth) { + pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth()); + } + + @Data + private static class PublicKeyResponse { + private String key; + } + + @Data + private static class SubscriptionRequest { + private String endpoint; + private String p256dh; + private String auth; + } +} diff --git a/src/main/java/com/openisle/model/PushSubscription.java b/src/main/java/com/openisle/model/PushSubscription.java new file mode 100644 index 000000000..34888513e --- /dev/null +++ b/src/main/java/com/openisle/model/PushSubscription.java @@ -0,0 +1,40 @@ +package com.openisle.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * Entity storing a browser push subscription for a user. + */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "push_subscriptions") +public class PushSubscription { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false, length = 512) + private String endpoint; + + @Column(nullable = false, length = 256) + private String p256dh; + + @Column(nullable = false, length = 256) + private String auth; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/openisle/repository/PushSubscriptionRepository.java b/src/main/java/com/openisle/repository/PushSubscriptionRepository.java new file mode 100644 index 000000000..28268d203 --- /dev/null +++ b/src/main/java/com/openisle/repository/PushSubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.openisle.repository; + +import com.openisle.model.PushSubscription; +import com.openisle.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PushSubscriptionRepository extends JpaRepository { + List findByUser(User user); + void deleteByUserAndEndpoint(User user, String endpoint); +} diff --git a/src/main/java/com/openisle/service/NotificationService.java b/src/main/java/com/openisle/service/NotificationService.java index 68e721887..ccfbecbbd 100644 --- a/src/main/java/com/openisle/service/NotificationService.java +++ b/src/main/java/com/openisle/service/NotificationService.java @@ -2,11 +2,14 @@ package com.openisle.service; import com.openisle.model.*; import com.openisle.repository.NotificationRepository; +import com.openisle.repository.ReactionRepository; import com.openisle.repository.UserRepository; import lombok.RequiredArgsConstructor; import com.openisle.service.EmailSender; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; import java.util.List; @@ -17,10 +20,29 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final UserRepository userRepository; private final EmailSender emailSender; + private final PushNotificationService pushNotificationService; + private final ReactionRepository reactionRepository; @Value("${app.website-url}") private String websiteUrl; + private String buildPayload(String body, String url) { +// try { +// return new ObjectMapper().writeValueAsString(Map.of( +// "body", body, +// "url", url +// )); +// } catch (Exception e) { +// return body; +// } + + return body; + } + + public void sendCustomPush(User user, String body, String url) { + pushNotificationService.sendNotification(user, buildPayload(body, url)); + } + public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) { return createNotification(user, type, post, comment, approved, null, null, null); } @@ -40,7 +62,27 @@ public class NotificationService { if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null) { String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId()); - emailSender.sendEmail(user.getEmail(), "【OpenIsle】有人回复了你", url); + String pushContent = comment.getAuthor() + "回复了你: \"" + comment.getContent() + "\""; + emailSender.sendEmail(user.getEmail(), "【OpenIsle】您有新的回复", pushContent + ", 点击以查看: " + url); + sendCustomPush(user, pushContent, url); + } else if (type == NotificationType.REACTION && comment != null) { + long count = reactionRepository.countReceived(comment.getAuthor().getUsername()); + if (count % 5 == 0) { + String url = websiteUrl + "/messages"; + sendCustomPush(comment.getAuthor(), "你有新的互动", url); + if (comment.getAuthor().getEmail() != null) { + emailSender.sendEmail(comment.getAuthor().getEmail(), "【OpenIsle】你有新的互动", "你有新的互动, 点击以查看: " + url); + } + } + } else if (type == NotificationType.REACTION && post != null) { + long count = reactionRepository.countReceived(post.getAuthor().getUsername()); + if (count % 5 == 0) { + String url = websiteUrl + "/messages"; + sendCustomPush(post.getAuthor(), "你有新的互动", url); + if (post.getAuthor().getEmail() != null) { + emailSender.sendEmail(post.getAuthor().getEmail(), "【OpenIsle】你有新的互动", "你有新的互动, 点击以查看: " + url); + } + } } return n; diff --git a/src/main/java/com/openisle/service/PushNotificationService.java b/src/main/java/com/openisle/service/PushNotificationService.java new file mode 100644 index 000000000..8f9336e2d --- /dev/null +++ b/src/main/java/com/openisle/service/PushNotificationService.java @@ -0,0 +1,44 @@ +package com.openisle.service; + +import com.openisle.model.PushSubscription; +import com.openisle.model.User; +import com.openisle.repository.PushSubscriptionRepository; +import lombok.extern.slf4j.Slf4j; +import nl.martijndwars.webpush.Notification; +import nl.martijndwars.webpush.PushService; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jose4j.lang.JoseException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Security; +import java.util.List; + +@Slf4j +@Service +public class PushNotificationService { + private final PushSubscriptionRepository subscriptionRepository; + private final PushService pushService; + + public PushNotificationService(PushSubscriptionRepository subscriptionRepository, + @Value("${app.webpush.public-key}") String publicKey, + @Value("${app.webpush.private-key}") String privateKey) throws GeneralSecurityException { + this.subscriptionRepository = subscriptionRepository; + Security.addProvider(new BouncyCastleProvider()); + this.pushService = new PushService(publicKey, privateKey); + } + + public void sendNotification(User user, String payload) { + List subs = subscriptionRepository.findByUser(user); + for (PushSubscription sub : subs) { + try { + Notification notification = new Notification(sub.getEndpoint(), sub.getP256dh(), sub.getAuth(), payload); + pushService.send(notification); + } catch (GeneralSecurityException | IOException | JoseException | InterruptedException | java.util.concurrent.ExecutionException e) { + log.error(e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/openisle/service/PushSubscriptionService.java b/src/main/java/com/openisle/service/PushSubscriptionService.java new file mode 100644 index 000000000..5a062b302 --- /dev/null +++ b/src/main/java/com/openisle/service/PushSubscriptionService.java @@ -0,0 +1,35 @@ +package com.openisle.service; + +import com.openisle.model.PushSubscription; +import com.openisle.model.User; +import com.openisle.repository.PushSubscriptionRepository; +import com.openisle.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PushSubscriptionService { + private final PushSubscriptionRepository subscriptionRepository; + private final UserRepository userRepository; + + @Transactional + public void saveSubscription(String username, String endpoint, String p256dh, String auth) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + subscriptionRepository.deleteByUserAndEndpoint(user, endpoint); + PushSubscription sub = new PushSubscription(); + sub.setUser(user); + sub.setEndpoint(endpoint); + sub.setP256dh(p256dh); + sub.setAuth(auth); + subscriptionRepository.save(sub); + } + + public List listByUser(User user) { + return subscriptionRepository.findByUser(user); + } +} diff --git a/src/main/java/com/openisle/service/ReactionService.java b/src/main/java/com/openisle/service/ReactionService.java index b752b828a..393de62b3 100644 --- a/src/main/java/com/openisle/service/ReactionService.java +++ b/src/main/java/com/openisle/service/ReactionService.java @@ -47,11 +47,6 @@ public class ReactionService { reaction = reactionRepository.save(reaction); if (!user.getId().equals(post.getAuthor().getId())) { notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null, user, type, null); - long count = reactionRepository.countReceived(post.getAuthor().getUsername()); - if (count % 5 == 0 && post.getAuthor().getEmail() != null) { - String url = websiteUrl + "/messages"; - emailSender.sendEmail(post.getAuthor().getEmail(), "【OpenIsle】你有新的互动", url); - } } return reaction; } @@ -75,11 +70,6 @@ public class ReactionService { reaction = reactionRepository.save(reaction); if (!user.getId().equals(comment.getAuthor().getId())) { notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, comment.getPost(), comment, null, user, type, null); - long count = reactionRepository.countReceived(comment.getAuthor().getUsername()); - if (count % 5 == 0 && comment.getAuthor().getEmail() != null) { - String url = websiteUrl + "/messages"; - emailSender.sendEmail(comment.getAuthor().getEmail(), "【OpenIsle】你有新的互动", url); - } } return reaction; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7d74a929c..f1e2b2b30 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -70,3 +70,7 @@ app.ai.format-limit=${AI_FORMAT_LIMIT:3} # Website URL for emails and redirects app.website-url=${WEBSITE_URL:https://www.open-isle.com} + +# Web push configuration +app.webpush.public-key=${WEBPUSH_PUBLIC_KEY:} +app.webpush.private-key=${WEBPUSH_PRIVATE_KEY:} diff --git a/src/test/java/com/openisle/controller/PushSubscriptionControllerTest.java b/src/test/java/com/openisle/controller/PushSubscriptionControllerTest.java new file mode 100644 index 000000000..fbb31b718 --- /dev/null +++ b/src/test/java/com/openisle/controller/PushSubscriptionControllerTest.java @@ -0,0 +1,36 @@ +package com.openisle.controller; + +import com.openisle.service.PushSubscriptionService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(PushSubscriptionController.class) +@AutoConfigureMockMvc(addFilters = false) +class PushSubscriptionControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PushSubscriptionService pushSubscriptionService; + + @Test + void subscribeEndpoint() throws Exception { + mockMvc.perform(post("/api/push/subscribe") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"endpoint\":\"e\",\"p256dh\":\"p\",\"auth\":\"a\"}") + .principal(new UsernamePasswordAuthenticationToken("u","p"))) + .andExpect(status().isOk()); + Mockito.verify(pushSubscriptionService).saveSubscription("u","e","p","a"); + } +} diff --git a/src/test/java/com/openisle/service/NotificationServiceTest.java b/src/test/java/com/openisle/service/NotificationServiceTest.java index 82bcd12e3..1dca76b33 100644 --- a/src/test/java/com/openisle/service/NotificationServiceTest.java +++ b/src/test/java/com/openisle/service/NotificationServiceTest.java @@ -3,6 +3,7 @@ package com.openisle.service; import com.openisle.model.*; import com.openisle.repository.NotificationRepository; import com.openisle.repository.UserRepository; +import com.openisle.service.PushNotificationService; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -19,7 +20,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); @@ -47,7 +49,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); @@ -69,7 +72,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); @@ -89,7 +93,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User admin = new User(); @@ -110,7 +115,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User admin = new User(); @@ -131,7 +137,8 @@ class NotificationServiceTest { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); EmailSender email = mock(EmailSender.class); - NotificationService service = new NotificationService(nRepo, uRepo, email); + PushNotificationService push = mock(PushNotificationService.class); + NotificationService service = new NotificationService(nRepo, uRepo, email, push); org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); @@ -145,5 +152,6 @@ class NotificationServiceTest { service.createNotification(user, NotificationType.COMMENT_REPLY, post, comment, null, null, null, null); verify(email).sendEmail("a@a.com", "【OpenIsle】有人回复了你", "https://ex.com/posts/1#comment-2"); + verify(push).sendNotification(eq(user), contains("/posts/1#comment-2")); } } diff --git a/src/test/java/com/openisle/service/ReactionServiceTest.java b/src/test/java/com/openisle/service/ReactionServiceTest.java index fea2e9fd9..9f797fee0 100644 --- a/src/test/java/com/openisle/service/ReactionServiceTest.java +++ b/src/test/java/com/openisle/service/ReactionServiceTest.java @@ -39,5 +39,6 @@ class ReactionServiceTest { service.reactToPost("bob", 3L, ReactionType.LIKE); verify(email).sendEmail("a@a.com", "【OpenIsle】你有新的互动", "https://ex.com/messages"); + verify(notif).sendCustomPush(author, "你有新的互动", "https://ex.com/messages"); } }