1+ package com .back .domain .notification .controller ;
2+
3+ import com .back .domain .notification .dto .NotificationGoResponseDto ;
4+ import com .back .domain .notification .dto .NotificationItemDto ;
5+ import com .back .domain .notification .dto .NotificationListResponseDto ;
6+ import com .back .domain .notification .dto .NotificationSettingDto ;
7+ import com .back .domain .notification .enums .NotificationType ;
8+ import com .back .domain .notification .service .NotificationService ;
9+ import com .back .domain .notification .service .NotificationSettingService ;
10+ import com .back .global .aspect .ResponseAspect ;
11+ import com .back .global .jwt .JwtUtil ;
12+ import com .back .global .rq .Rq ;
13+ import com .back .global .security .SecurityUser ;
14+ import java .time .LocalDateTime ;
15+ import java .util .List ;
16+ import java .util .Map ;
17+ import org .junit .jupiter .api .AfterEach ;
18+ import org .junit .jupiter .api .DisplayName ;
19+ import org .junit .jupiter .api .Test ;
20+ import org .springframework .beans .factory .annotation .Autowired ;
21+ import org .springframework .boot .autoconfigure .ImportAutoConfiguration ;
22+ import org .springframework .boot .autoconfigure .aop .AopAutoConfiguration ;
23+ import org .springframework .boot .test .autoconfigure .web .servlet .AutoConfigureMockMvc ;
24+ import org .springframework .boot .test .autoconfigure .web .servlet .WebMvcTest ;
25+ import org .springframework .context .annotation .Import ;
26+ import org .springframework .http .MediaType ;
27+ import org .springframework .security .authentication .UsernamePasswordAuthenticationToken ;
28+ import org .springframework .security .core .authority .SimpleGrantedAuthority ;
29+ import org .springframework .security .core .context .SecurityContext ;
30+ import org .springframework .security .core .context .SecurityContextHolder ;
31+ import org .springframework .security .web .context .HttpSessionSecurityContextRepository ;
32+ import org .springframework .test .context .bean .override .mockito .MockitoBean ;
33+ import org .springframework .test .web .servlet .MockMvc ;
34+ import org .springframework .test .web .servlet .MvcResult ;
35+ import org .springframework .test .web .servlet .request .RequestPostProcessor ;
36+ import org .springframework .web .servlet .mvc .method .annotation .SseEmitter ;
37+
38+ import static org .mockito .ArgumentMatchers .eq ;
39+ import static org .mockito .BDDMockito .given ;
40+ import static org .mockito .Mockito .verify ;
41+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .asyncDispatch ;
42+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .get ;
43+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .patch ;
44+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .post ;
45+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .jsonPath ;
46+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .request ;
47+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .status ;
48+
49+ @ WebMvcTest (NotificationController .class )
50+ @ AutoConfigureMockMvc (addFilters = false )
51+ @ Import (ResponseAspect .class )
52+ @ ImportAutoConfiguration (AopAutoConfiguration .class )
53+ class NotificationControllerTest {
54+
55+ @ Autowired
56+ private MockMvc mockMvc ;
57+
58+ @ MockitoBean
59+ private NotificationService notificationService ;
60+
61+ @ MockitoBean
62+ private NotificationSettingService notificationSettingService ;
63+
64+ @ MockitoBean
65+ private JwtUtil jwtUtil ;
66+
67+ @ MockitoBean
68+ private Rq rq ;
69+
70+ @ AfterEach
71+ void clearSecurityContext () {
72+ SecurityContextHolder .clearContext ();
73+ }
74+
75+ private SecurityUser createPrincipal (Long userId ) {
76+ return new SecurityUser (
77+ userId ,
78+ "user" + userId + "@example.com" ,
79+ "user" + userId ,
80+ false ,
81+ List .of (new SimpleGrantedAuthority ("ROLE_USER" )),
82+ Map .of ()
83+ );
84+ }
85+
86+ private UsernamePasswordAuthenticationToken authenticated (SecurityUser principal ) {
87+ return new UsernamePasswordAuthenticationToken (principal , null , principal .getAuthorities ());
88+ }
89+
90+ private RequestPostProcessor withPrincipal (SecurityUser principal ) {
91+ return request -> {
92+ UsernamePasswordAuthenticationToken authentication = authenticated (principal );
93+ SecurityContext context = SecurityContextHolder .createEmptyContext ();
94+ context .setAuthentication (authentication );
95+ SecurityContextHolder .setContext (context );
96+ request .setUserPrincipal (authentication );
97+ request .getSession (true )
98+ .setAttribute (HttpSessionSecurityContextRepository .SPRING_SECURITY_CONTEXT_KEY , context );
99+ return request ;
100+ };
101+ }
102+
103+ @ Test
104+ @ DisplayName ("Subscribe SSE" )
105+ void subscribe_success () throws Exception {
106+ SseEmitter emitter = new SseEmitter (0L );
107+ emitter .complete ();
108+ given (notificationService .subscribe ()).willReturn (emitter );
109+
110+ MvcResult result = mockMvc .perform (get ("/me/subscribe" )
111+ .with (withPrincipal (createPrincipal (1L )))
112+ .accept (MediaType .TEXT_EVENT_STREAM ))
113+ .andExpect (request ().asyncStarted ())
114+ .andReturn ();
115+
116+ mockMvc .perform (asyncDispatch (result ))
117+ .andExpect (status ().isOk ());
118+
119+ verify (notificationService ).subscribe ();
120+ }
121+
122+ @ Test
123+ @ DisplayName ("Get notifications without cursor" )
124+ void getNotifications_noCursor () throws Exception {
125+ SecurityUser principal = createPrincipal (3L );
126+
127+ NotificationItemDto item = NotificationItemDto .builder ()
128+ .id (101L )
129+ .type (NotificationType .COMMENT )
130+ .postId (55L )
131+ .postTitle ("새 댓글" )
132+ .read (false )
133+ .createdAt (LocalDateTime .of (2025 , 1 , 2 , 12 , 0 ))
134+ .build ();
135+
136+ NotificationListResponseDto responseDto = new NotificationListResponseDto (
137+ List .of (item ),
138+ false ,
139+ null ,
140+ null
141+ );
142+
143+ given (notificationService .getNotifications (
144+ principal .getId (),
145+ null ,
146+ null ,
147+ 20
148+ )).willReturn (responseDto );
149+
150+ mockMvc .perform (get ("/me/notifications" )
151+ .with (withPrincipal (principal ))
152+ .accept (MediaType .APPLICATION_JSON ))
153+ .andExpect (status ().isOk ())
154+ .andExpect (jsonPath ("$.code" ).value (200 ))
155+ .andExpect (jsonPath ("$.message" ).value ("success" ))
156+ .andExpect (jsonPath ("$.data.items[0].id" ).value (101 ))
157+ .andExpect (jsonPath ("$.data.hasNext" ).value (false ))
158+ .andExpect (jsonPath ("$.data.nextCreatedAt" ).doesNotExist ());
159+
160+ verify (notificationService ).getNotifications (principal .getId (), null , null , 20 );
161+ }
162+
163+ @ Test
164+ @ DisplayName ("Get notifications with cursor" )
165+ void getNotifications_withCursor () throws Exception {
166+ SecurityUser principal = createPrincipal (5L );
167+ LocalDateTime cursor = LocalDateTime .of (2025 , 3 , 10 , 9 , 30 , 15 );
168+
169+ NotificationListResponseDto responseDto = new NotificationListResponseDto (
170+ List .of (),
171+ true ,
172+ cursor .minusHours (1 ),
173+ 88L
174+ );
175+
176+ given (notificationService .getNotifications (
177+ eq (principal .getId ()),
178+ eq (cursor ),
179+ eq (77L ),
180+ eq (5 )
181+ )).willReturn (responseDto );
182+
183+ mockMvc .perform (get ("/me/notifications" )
184+ .with (withPrincipal (principal ))
185+ .param ("lastCreatedAt" , cursor .toString ())
186+ .param ("lastId" , "77" )
187+ .param ("limit" , "5" )
188+ .accept (MediaType .APPLICATION_JSON ))
189+ .andExpect (status ().isOk ())
190+ .andExpect (jsonPath ("$.data.hasNext" ).value (true ))
191+ .andExpect (jsonPath ("$.data.nextId" ).value (88 ));
192+
193+ verify (notificationService ).getNotifications (principal .getId (), cursor , 77L , 5 );
194+ }
195+
196+ @ Test
197+ @ DisplayName ("Get notification setting" )
198+ void getNotificationSetting () throws Exception {
199+ SecurityUser principal = createPrincipal (9L );
200+ NotificationSettingDto dto = new NotificationSettingDto (true );
201+ given (notificationSettingService .getMySetting (principal .getId ())).willReturn (dto );
202+
203+ mockMvc .perform (get ("/me/notification-setting" )
204+ .with (withPrincipal (principal ))
205+ .accept (MediaType .APPLICATION_JSON ))
206+ .andExpect (status ().isOk ())
207+ .andExpect (jsonPath ("$.data.enabled" ).value (true ));
208+
209+ verify (notificationSettingService ).getMySetting (principal .getId ());
210+ }
211+
212+ @ Test
213+ @ DisplayName ("Patch notification setting" )
214+ void patchNotificationSetting () throws Exception {
215+ SecurityUser principal = createPrincipal (11L );
216+ NotificationSettingDto dto = new NotificationSettingDto (false );
217+ given (notificationSettingService .setMySetting (eq (principal .getId ()), eq (false ))).willReturn (dto );
218+
219+ mockMvc .perform (patch ("/me/notification-setting" )
220+ .with (withPrincipal (principal ))
221+ .contentType (MediaType .APPLICATION_JSON )
222+ .content ("{\" enabled\" :false}" )
223+ .accept (MediaType .APPLICATION_JSON ))
224+ .andExpect (status ().isOk ())
225+ .andExpect (jsonPath ("$.data.enabled" ).value (false ));
226+
227+ verify (notificationSettingService ).setMySetting (principal .getId (), false );
228+ }
229+
230+ @ Test
231+ @ DisplayName ("Go post link and mark read" )
232+ void goPostLink () throws Exception {
233+ SecurityUser principal = createPrincipal (13L );
234+ NotificationGoResponseDto dto = new NotificationGoResponseDto (321L , "/posts/321" );
235+ given (notificationService .markAsReadAndGetPostLink (eq (principal .getId ()), eq (999L ))).willReturn (dto );
236+
237+ mockMvc .perform (post ("/me/notifications/{id}" , 999L )
238+ .with (withPrincipal (principal ))
239+ .accept (MediaType .APPLICATION_JSON ))
240+ .andExpect (status ().isOk ())
241+ .andExpect (jsonPath ("$.data.postId" ).value (321 ))
242+ .andExpect (jsonPath ("$.data.postApiUrl" ).value ("/posts/321" ));
243+
244+ verify (notificationService ).markAsReadAndGetPostLink (principal .getId (), 999L );
245+ }
246+ }
0 commit comments