View Javadoc
1   package fr.tiogars.chat.domains.task.controllers;
2   
3   import static org.mockito.ArgumentMatchers.any;
4   import static org.mockito.ArgumentMatchers.eq;
5   import static org.mockito.Mockito.when;
6   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
7   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
9   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
10  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
11  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
12  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
13  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
14  
15  import java.time.Instant;
16  import java.sql.SQLException;
17  import java.util.List;
18  import java.util.Optional;
19  
20  import org.junit.jupiter.api.Test;
21  import org.hibernate.exception.ConstraintViolationException;
22  import org.springframework.dao.DataIntegrityViolationException;
23  import org.springframework.dao.OptimisticLockingFailureException;
24  import org.springframework.beans.factory.annotation.Autowired;
25  import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
26  import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
27  import org.springframework.context.annotation.Import;
28  import org.springframework.http.HttpHeaders;
29  import org.springframework.http.MediaType;
30  import org.springframework.security.core.userdetails.UserDetailsService;
31  import org.springframework.test.context.bean.override.mockito.MockitoBean;
32  import org.springframework.test.web.servlet.MockMvc;
33  
34  import fr.tiogars.chat.common.models.DeleteSelectedResponse;
35  import fr.tiogars.chat.config.ApiCorsConfiguration;
36  import fr.tiogars.chat.domains.auth.services.JwtTokenService;
37  import fr.tiogars.chat.domains.task.models.CreateTaskRequest;
38  import fr.tiogars.chat.domains.task.models.Task;
39  import fr.tiogars.chat.domains.task.models.UpdateTaskRequest;
40  import fr.tiogars.chat.domains.task.services.TaskCsvCodec;
41  import fr.tiogars.chat.domains.task.services.TaskCreateService;
42  import fr.tiogars.chat.domains.task.services.TaskDeleteSelectedService;
43  import fr.tiogars.chat.domains.task.services.TaskDeleteService;
44  import fr.tiogars.chat.domains.task.services.TaskFindAllService;
45  import fr.tiogars.chat.domains.task.services.TaskFindByIdService;
46  import fr.tiogars.chat.domains.task.services.TaskSeedService;
47  import fr.tiogars.chat.domains.task.services.TaskUpdateService;
48  
49  @WebMvcTest({
50                  TaskCreateController.class,
51                  TaskUpdateController.class,
52                  TasksListController.class,
53                  TaskReadController.class,
54                  TaskDeleteController.class,
55                  TaskDeleteSelectedController.class,
56                  TaskSeedController.class,
57                  TaskImportExportController.class
58  })
59  @AutoConfigureMockMvc(addFilters = false)
60  @Import(ApiCorsConfiguration.class)
61  public class TaskControllerTest {
62  
63          @Autowired
64          private MockMvc mockMvc;
65  
66          @MockitoBean
67          private TaskCreateService createService;
68  
69          @MockitoBean
70          private TaskUpdateService updateService;
71  
72          @MockitoBean
73          private TaskDeleteService deleteService;
74  
75          @MockitoBean
76          private TaskFindAllService findAllService;
77  
78          @MockitoBean
79          private TaskFindByIdService findByIdService;
80  
81          @MockitoBean
82          private TaskSeedService seedService;
83  
84          @MockitoBean
85          private TaskCsvCodec csvCodec;
86  
87          @MockitoBean
88          private TaskDeleteSelectedService deleteSelectedService;
89  
90          @MockitoBean
91          private JwtTokenService jwtTokenService;
92  
93          @MockitoBean
94          private UserDetailsService userDetailsService;
95  
96          @Test
97          public void deleteSelected_returnsDeletedAndSkippedIds() throws Exception {
98                  when(deleteSelectedService.deleteSelected(List.of(1L, 2L)))
99                                  .thenReturn(new DeleteSelectedResponse(List.of(1L), List.of(2L)));
100 
101                 mockMvc.perform(post("/api/v1/task/delete-selected")
102                                 .contentType(MediaType.APPLICATION_JSON)
103                                 .content("{\"ids\":[1,2]}"))
104                                 .andExpect(status().isOk())
105                                 .andExpect(jsonPath("$.deletedIds[0]").value(1))
106                                 .andExpect(jsonPath("$.skippedIds[0]").value(2));
107         }
108 
109         @Test
110         public void deleteSelected_rejectsEmptySelection() throws Exception {
111                 mockMvc.perform(post("/api/v1/task/delete-selected")
112                                 .contentType(MediaType.APPLICATION_JSON)
113                                 .content("{\"ids\":[]}"))
114                                 .andExpect(status().isBadRequest());
115         }
116 
117         @Test
118         public void create_returnsConflict_whenConcurrentWriterClaimsCode() throws Exception {
119                 when(createService.create(any(CreateTaskRequest.class)))
120                                 .thenThrow(new DataIntegrityViolationException("Duplicate active code",
121                                                 new ConstraintViolationException("Duplicate active code",
122                                                                 new SQLException(), "uk_tasks_active_code")));
123 
124                 mockMvc.perform(post("/api/v1/task")
125                                 .contentType(MediaType.APPLICATION_JSON)
126                                 .content("{\"code\":\"CUSTOMER\",\"label\":\"Customer\"}"))
127                                 .andExpect(status().isConflict());
128         }
129 
130         @Test
131         public void update_returnsConflict_whenConcurrentWriterChangesRow() throws Exception {
132                 when(updateService.update(eq(42L), any(UpdateTaskRequest.class)))
133                                 .thenThrow(new OptimisticLockingFailureException("Concurrent modification"));
134 
135                 mockMvc.perform(put("/api/v1/task/42")
136                                 .contentType(MediaType.APPLICATION_JSON)
137                                 .content("{\"label\":\"Customer\"}"))
138                                 .andExpect(status().isConflict());
139         }
140 
141         @Test
142         public void create_returnsCreatedTemplateAndLocation() throws Exception {
143                 when(createService.create(any(CreateTaskRequest.class))).thenReturn(Optional.of(template()));
144 
145                 mockMvc.perform(post("/api/v1/task")
146                                 .contentType(MediaType.APPLICATION_JSON)
147                                 .content("{\"code\":\"CUSTOMER\",\"label\":\"Customer\",\"description\":\"Example\"}"))
148                                 .andExpect(status().isCreated())
149                                 .andExpect(header().string("Location", "/api/v1/task/42"))
150                                 .andExpect(jsonPath("$.code").value("CUSTOMER"));
151         }
152 
153         @Test
154         public void create_rejectsBlankCode() throws Exception {
155                 mockMvc.perform(post("/api/v1/task")
156                                 .contentType(MediaType.APPLICATION_JSON)
157                                 .content("{\"code\":\" \",\"label\":\"Customer\"}"))
158                                 .andExpect(status().isBadRequest());
159         }
160 
161         @Test
162         public void findAllAndFindById_returnTemplates() throws Exception {
163                 when(findAllService.findAll()).thenReturn(List.of(template()));
164                 when(findByIdService.findById(42L)).thenReturn(Optional.of(template()));
165 
166                 mockMvc.perform(get("/api/v1/task"))
167                                 .andExpect(status().isOk())
168                                 .andExpect(jsonPath("$[0].id").value(42))
169                                 .andExpect(jsonPath("$[0].createdBy").value("creator"))
170                                 .andExpect(jsonPath("$[0].lastModifiedBy").value("editor"));
171                 mockMvc.perform(get("/api/v1/task/42"))
172                                 .andExpect(status().isOk())
173                                 .andExpect(jsonPath("$.label").value("Customer"))
174                                 .andExpect(jsonPath("$.createdBy").value("creator"))
175                                 .andExpect(jsonPath("$.lastModifiedBy").value("editor"));
176         }
177 
178         @Test
179         public void findAll_allowsBackendConsoleOrigin() throws Exception {
180                 when(findAllService.findAll()).thenReturn(List.of(template()));
181 
182                 mockMvc.perform(get("/api/v1/task")
183                                 .header(HttpHeaders.ORIGIN, "http://localhost:5173"))
184                                 .andExpect(status().isOk())
185                                 .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
186                                                 "http://localhost:5173"));
187         }
188 
189         @Test
190         public void updateAndDelete_usePersistentIdentifier() throws Exception {
191                 when(updateService.update(eq(42L), any(UpdateTaskRequest.class)))
192                                 .thenReturn(Optional.of(template()));
193                 when(deleteService.delete(42L)).thenReturn(true);
194 
195                 mockMvc.perform(put("/api/v1/task/42")
196                                 .contentType(MediaType.APPLICATION_JSON)
197                                 .content("{\"label\":\"Customer\",\"description\":\"Updated\"}"))
198                                 .andExpect(status().isOk());
199                 mockMvc.perform(delete("/api/v1/task/42"))
200                                 .andExpect(status().isNoContent());
201         }
202 
203         @Test
204         public void seedAndImportExport_useNewRoutes() throws Exception {
205                 when(seedService.seed(any())).thenReturn(List.of(template()));
206                 when(findAllService.findAll()).thenReturn(List.of(template()));
207                 when(csvCodec.serialize(any())).thenReturn("id,code,label,description,createdDate,lastModifiedDate\n");
208                 when(csvCodec.parse(any())).thenReturn(List.of(
209                                 new CreateTaskRequest("CUSTOMER", "Customer", null)));
210 
211                 mockMvc.perform(post("/api/v1/task/seed")
212                                 .contentType(MediaType.APPLICATION_JSON)
213                                 .content("[{\"code\":\"CUSTOMER\",\"label\":\"Customer\"}]"))
214                                 .andExpect(status().isCreated());
215                 mockMvc.perform(get("/api/v1/task/export.csv"))
216                                 .andExpect(status().isOk())
217                                 .andExpect(content().contentTypeCompatibleWith("text/csv"));
218                 mockMvc.perform(post("/api/v1/task/import.csv")
219                                 .contentType("text/csv")
220                                 .content("id,code,label,description,createdDate,lastModifiedDate\n"))
221                                 .andExpect(status().isCreated());
222         }
223 
224         private static Task template() {
225                 Instant timestamp = Instant.parse("2026-08-30T12:00:00Z");
226                 return new Task(42L, "CUSTOMER", "Customer", "Example", timestamp, timestamp,
227                                 "creator", "editor");
228         }
229 }