Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.swyp.picke.domain.admin.controller;

import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeCreateRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationTestRequest;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDetailResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeListResponse;
import com.swyp.picke.domain.admin.service.AdminNotificationService;
import com.swyp.picke.domain.notification.enums.NotificationCategory;
import com.swyp.picke.domain.notification.service.NotificationDispatchService;
import com.swyp.picke.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -27,6 +29,7 @@
public class AdminNotificationController {

private final AdminNotificationService adminNotificationService;
private final NotificationDispatchService notificationDispatchService;

@Operation(summary = "공지사항 작성")
@PostMapping
Expand All @@ -51,4 +54,13 @@ public ApiResponse<AdminNoticeListResponse> getNotices(
public ApiResponse<AdminNoticeDetailResponse> getNoticeDetail(@PathVariable Long noticeId) {
return ApiResponse.onSuccess(adminNotificationService.getNoticeDetail(noticeId));
}

@Operation(summary = "푸시 알림 발송 테스트", description = "특정 유저의 등록된 디바이스로 알림 설정(ON/OFF) 무관하게 즉시 테스트 푸시를 발송한다.")
@PostMapping("/test")
public ApiResponse<Void> sendTestPush(
@RequestBody @Valid AdminNotificationTestRequest request
) {
notificationDispatchService.sendTestPush(request.userId(), request.title(), request.body());
return ApiResponse.onSuccess(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.swyp.picke.domain.admin.dto.notification.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record AdminNotificationTestRequest(
@NotNull Long userId,
@NotBlank String title,
@NotBlank String body
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ public void notifyAdminNotice(NotificationDetailCode detailCode, String title, S
}
}

/**
* 관리자가 특정 유저에게 알림 설정(ON/OFF) 체크 없이 즉시 테스트 푸시를 발송한다.
*/
public void sendTestPush(Long userId, String title, String body) {
Map<String, String> data = Map.of("type", "TEST");

for (UserDevice device : userDeviceRepository.findAllByUserId(userId)) {
sendPush(device, title, body, data);
}
}

/**
* 유저의 알림 ON/OFF 설정을 확인한다. 설정이 없는 경우(레거시 유저 등) 기본값은 true.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.swyp.picke.domain.admin.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.swyp.picke.domain.notification.entity.UserDevice;
import com.swyp.picke.domain.notification.enums.DevicePlatform;
import com.swyp.picke.domain.notification.repository.UserDeviceRepository;
import com.swyp.picke.domain.oauth.jwt.JwtProvider;
import com.swyp.picke.domain.user.entity.User;
import com.swyp.picke.domain.user.enums.UserRole;
import com.swyp.picke.domain.user.enums.UserStatus;
import com.swyp.picke.domain.user.repository.UserRepository;
import com.swyp.picke.global.infra.apns.service.ApnsPushService;
import com.swyp.picke.global.infra.fcm.service.FcmPushService;
import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import software.amazon.awssdk.services.s3.S3Client;

import java.util.Map;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AdminNotificationTestPushIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@Autowired
private JwtProvider jwtProvider;

@Autowired
private UserRepository userRepository;

@Autowired
private UserDeviceRepository userDeviceRepository;

@MockitoBean
private S3Client s3Client;

@MockitoBean
private S3PresignedUrlService s3PresignedUrlService;

@MockitoBean
private FcmPushService fcmPushService;

@MockitoBean
private ApnsPushService apnsPushService;

@Test
@DisplayName("admin can send a test push to a specific user's registered device")
void admin_can_send_test_push_to_user_device() throws Exception {
String adminToken = createAdminToken();

User targetUser = userRepository.save(
User.builder()
.userTag("target-" + UUID.randomUUID().toString().substring(0, 8))
.nickname("target")
.role(UserRole.USER)
.status(UserStatus.ACTIVE)
.build()
);

UserDevice device = userDeviceRepository.save(
UserDevice.builder()
.user(targetUser)
.fcmToken("test-token-" + UUID.randomUUID())
.platform(DevicePlatform.ANDROID)
.build()
);

Map<String, Object> payload = Map.of(
"userId", targetUser.getId(),
"title", "테스트 알림",
"body", "테스트 발송 본문"
);

mockMvc.perform(post("/api/v1/admin/notices/test")
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(payload)))
.andExpect(status().isOk());

ArgumentCaptor<UserDevice> deviceCaptor = ArgumentCaptor.forClass(UserDevice.class);
verify(fcmPushService).send(deviceCaptor.capture(), eq("테스트 알림"), eq("테스트 발송 본문"), any());
assertThat(deviceCaptor.getValue().getId()).isEqualTo(device.getId());
}

private String createAdminToken() {
User admin = userRepository.save(
User.builder()
.userTag("adm-" + UUID.randomUUID().toString().substring(0, 8))
.nickname("adm")
.role(UserRole.ADMIN)
.status(UserStatus.ACTIVE)
.build()
);
return jwtProvider.createAccessToken(admin.getId(), UserRole.ADMIN.name());
}
}
Loading