From e06f541f487f033ae3c7598fd6cccc8e41ab9629 Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Wed, 10 Sep 2025 19:02:38 +0200 Subject: [PATCH 1/9] fix: issue with comparing dates in ExamControllerTest --- .../examCalendar/ExamControllerTest.java | 270 +++++++++--------- 1 file changed, 142 insertions(+), 128 deletions(-) diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java index 22692bb..5c8b9a8 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java @@ -48,34 +48,34 @@ @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) @ActiveProfiles("database") class ExamControllerTest { - + @Autowired private MockMvc mockMvc; - + @Autowired private ExamTypeRepository examTypeRepository; - + @Autowired private ExamRepository examRepository; - + @Autowired private ObjectMapper mapper; - + @Autowired private GroupRepository groupRepository; - + @Mock private TimetableService timetableService; - + @BeforeEach void setupBeforeEach () { examRepository.deleteAll(); examTypeRepository.deleteAll(); groupRepository.deleteAll(); } - + // - + /** * check if addExam endpoint create new exam with correct URI and correct data */ @@ -86,10 +86,10 @@ void addExamWithCorrectData () throws Exception { createExampleExamType("Project"); ExamDto examDtoRequest = createExampleExamDto("Project"); String json = mapper.writeValueAsString(examDtoRequest); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + MvcResult result = mockMvc .perform(MockMvcRequestBuilders .post("/pkwmtt/api/v1/exams") @@ -99,12 +99,12 @@ void addExamWithCorrectData () throws Exception { .andExpect(status().isCreated()) .andExpect(header().string("Location", containsString("/pkwmtt/api/v1/exams/"))) .andReturn(); - + String location = result.getResponse().getHeader("Location"); @SuppressWarnings("DataFlowIssue") int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1)); - + Exam examResponse = examRepository.findById(id).orElseThrow(); - + Set responseSubgroups = examResponse .getGroups() .stream() @@ -115,10 +115,10 @@ void addExamWithCorrectData () throws Exception { .filter(g -> g.matches("^\\d.*")) .collect(Collectors.toSet()); responseSubgroups.removeAll(responseGeneralGroups); - + assertEquals(responseGeneralGroups, Set.of("12K")); assertEquals(responseSubgroups, examDtoRequest.getSubgroups()); - + assertEquals(examDtoRequest.getTitle(), examResponse.getTitle()); assertEquals(examDtoRequest.getDescription(), examResponse.getDescription()); // compare dates with minutes level precision @@ -126,20 +126,20 @@ void addExamWithCorrectData () throws Exception { examDtoRequest.getDate().truncatedTo(ChronoUnit.MINUTES), examResponse.getExamDate().truncatedTo(ChronoUnit.MINUTES) ); - + assertEquals(examDtoRequest.getExamType(), examResponse.getExamType().getName()); } - + @Test @Transactional void addExamTwice () throws Exception { // given createExampleExamType("Project"); ExamDto examDtoRequest = createExampleExamDto("Project"); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + // when assertPostRequest(status().isCreated(), examDtoRequest); MvcResult result = assertPostRequest(status().isConflict(), examDtoRequest); @@ -147,7 +147,7 @@ void addExamTwice () throws Exception { assertResponseMessage("Exam already exists", result); assertEquals(1, examRepository.findAllByTitle(examDtoRequest.getTitle()).size()); } - + @Test void addExamWithBlankExamTitle () throws Exception { // given @@ -162,11 +162,11 @@ void addExamWithBlankExamTitle () throws Exception { .build(); // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("title : must not be blank", result); } - + @Test void addExamWithBlankExamDescription () throws Exception { // given @@ -179,19 +179,19 @@ void addExamWithBlankExamDescription () throws Exception { .generalGroups(Set.of("12K2")) .subgroups(Set.of("L04")) .build(); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); // when MvcResult result = assertPostRequest(status().isCreated(), requestData); - + String location = result.getResponse().getHeader("Location"); @SuppressWarnings("DataFlowIssue") int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1)); - + Exam examResponse = examRepository.findById(id).orElseThrow(); assertNull(examResponse.getDescription()); } - + @Test void addExamWithBlankDate () throws Exception { // given @@ -206,11 +206,11 @@ void addExamWithBlankDate () throws Exception { .build(); // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("date : must not be null", result); } - + @Test void addExamWithBlankExamGroups () throws Exception { // given @@ -222,14 +222,14 @@ void addExamWithBlankExamGroups () throws Exception { .date(LocalDateTime.now().plusDays(1)) .examType("Project") .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("generalGroups : must not be empty", result); } - + @Test void addExamWithBlankGeneralGroups () throws Exception { // given @@ -243,13 +243,13 @@ void addExamWithBlankGeneralGroups () throws Exception { // null generalGroups .subgroups(Set.of("L04")) .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); // then assertResponseMessage("generalGroups : must not be empty", result); } - + @Test @Transactional void addExamWithBlankSubgroups () throws Exception { @@ -264,20 +264,20 @@ void addExamWithBlankSubgroups () throws Exception { .generalGroups(Set.of("12K2")) // null subgroups .build(); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + // when MvcResult result = assertPostRequest(status().isCreated(), requestData); // then String location = result.getResponse().getHeader("Location"); @SuppressWarnings("DataFlowIssue") int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1)); Exam examResponse = examRepository.findById(id).orElseThrow(); - + assertEquals("12K2", examResponse.getGroups().iterator().next().getName()); } - + @Test void addExamWithMultipleGeneralGroupsAndSubgroups () throws Exception { // given @@ -291,16 +291,16 @@ void addExamWithMultipleGeneralGroupsAndSubgroups () throws Exception { .generalGroups(Set.of("12K1", "12K2")) .subgroups(Set.of("L04")) .build(); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); // then assertResponseMessage("Invalid group identifier: ambiguous general groups for subgroups", result); } - + @Test void addExamWithNullExamTypes () throws Exception { // given @@ -314,14 +314,14 @@ void addExamWithNullExamTypes () throws Exception { .subgroups(Set.of("L04")) // no examType .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("examType : must not be null", result); } - + @Test void addExamWithNotFutureDate () throws Exception { // given @@ -337,11 +337,11 @@ void addExamWithNotFutureDate () throws Exception { .build(); // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("date : Date must be in the future", result); } - + @Test void addExamWithEmptyStringExamTitle () throws Exception { // given @@ -355,14 +355,14 @@ void addExamWithEmptyStringExamTitle () throws Exception { .generalGroups(Set.of("12K2")) .subgroups(Set.of("L04")) .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("title : must not be blank", result); } - + @Test void addExamWithTooLongExamTitle () throws Exception { // given @@ -376,14 +376,14 @@ void addExamWithTooLongExamTitle () throws Exception { .generalGroups(Set.of("12K2")) .subgroups(Set.of("L04")) .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("title : max size of field is 255", result); } - + @Test void addExamWithTooLongDescription () throws Exception { // given @@ -397,14 +397,14 @@ void addExamWithTooLongDescription () throws Exception { .generalGroups(Set.of("12K2")) .subgroups(Set.of("L04")) .build(); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("description : max size of field is 255", result); } - + @Test void addExamWithNonExistingExamType () throws Exception { // given @@ -418,39 +418,40 @@ void addExamWithNonExistingExamType () throws Exception { .generalGroups(Set.of("12K2")) .subgroups(Set.of("L04")) .build(); - + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + // when MvcResult result = assertPostRequest(status().isBadRequest(), requestData); - + // then assertResponseMessage("Invalid exam type NonExistingExamType", result); } - - + + // - + // @Test @Transactional void modifyExamWithCorrectData () throws Exception { // given + LocalDateTime date = LocalDateTime.now().plusDays(1); ExamType examType = createExampleExamType("Exam"); Exam exam = createExampleExam(examType); int id = examRepository.save(exam).getExamId(); - ExamDto examDto = createExampleExamDto(examType.getName()); - + ExamDto examDto = createExampleExamDto(examType.getName(), date); + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1", "12K2", "12K3")); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "L04", "P04")); - + // when assertPutRequest(status().isNoContent(), examDto, id); - + // then Exam responseExam = examRepository.findById(id).orElseThrow(); - + Set responseSubgroups = responseExam .getGroups() .stream() @@ -461,17 +462,14 @@ void modifyExamWithCorrectData () throws Exception { .filter(g -> g.matches("^\\d.*")) .collect(Collectors.toSet()); responseSubgroups.removeAll(responseGeneralGroups); - + assertEquals("Math exam", responseExam.getTitle()); assertEquals("first exam", responseExam.getDescription()); -// assertEquals( -// LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES), -// responseExam.getExamDate().truncatedTo(ChronoUnit.MINUTES) -// ); + assertEquals(date, responseExam.getExamDate()); assertEquals(Set.of("12K"), responseGeneralGroups); assertEquals(Set.of("L04"), responseSubgroups); } - + @Test void modifyExamWithIncorrectExamId () throws Exception { // given @@ -479,18 +477,18 @@ void modifyExamWithIncorrectExamId () throws Exception { Exam exam = createExampleExam(examType); int id = examRepository.save(exam).getExamId(); ExamDto examDto = createExampleExamDto(examType.getName()); - + int invalidId = Integer.MAX_VALUE - 10; assertNotEquals(invalidId, id); // when MvcResult result = assertPutRequest(status().isNotFound(), examDto, invalidId); - + // then assertResponseMessage("No such element with id: " + (invalidId), result); - + } // - + // @Test void deleteExamWithCorrectArguments () throws Exception { @@ -498,14 +496,14 @@ void deleteExamWithCorrectArguments () throws Exception { ExamType examType = createExampleExamType("Exam"); Exam exam = createExampleExam(examType); int id = examRepository.save(exam).getExamId(); - + // when assertDeleteRequest(status().isNoContent(), id); - + // then assertTrue(examRepository.findById(id).isEmpty()); } - + @Test void deleteNonExistingExam () throws Exception { // given @@ -514,30 +512,30 @@ void deleteNonExistingExam () throws Exception { int id = examRepository.save(exam).getExamId(); int invalidId = Integer.MAX_VALUE - 10; assertNotEquals(invalidId, id); - + // when MvcResult result = assertDeleteRequest(status().isNotFound(), invalidId); - + // then assertTrue(examRepository.findById(id).isPresent()); assertResponseMessage("No such element with id: " + (invalidId), result); } - + // - + // - + @Test void getExamByIdWithCorrectId () throws Exception { // given ExamType examType = createExampleExamType("Exam"); Exam exam = createExampleExam(examType); int id = examRepository.save(exam).getExamId(); - + // when MvcResult result = assertGetByIdRequest(status().isOk(), id); JsonNode responseNode = mapper.readTree(result.getResponse().getContentAsString()); - + // then assertEquals(exam.getTitle(), responseNode.get("title").asText()); assertEquals(exam.getDescription(), responseNode.get("description").asText()); @@ -551,7 +549,7 @@ void getExamByIdWithCorrectId () throws Exception { responseNode.get("examType") ); } - + @Test void getNonExistingExamById () throws Exception { // given @@ -560,16 +558,16 @@ void getNonExistingExamById () throws Exception { int id = examRepository.save(exam).getExamId(); int invalidId = Integer.MAX_VALUE - 10; assertNotEquals(invalidId, id); - + // when MvcResult result = assertGetByIdRequest(status().isNotFound(), invalidId); - + // then assertResponseMessage("No such element with id: " + (invalidId), result); } - + // - + @Test void getExamsWithGeneralGroups () throws Exception { // given @@ -577,10 +575,10 @@ void getExamsWithGeneralGroups () throws Exception { Exam exam2 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex2", Set.of("12K2", "12K1"))); Exam exam3 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex3", Set.of("12A2"))); Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04"))); - + // when MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("12K2")); - + // then JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString()); assertEquals(2, responseArray.size()); @@ -589,7 +587,7 @@ void getExamsWithGeneralGroups () throws Exception { assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle()))); assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam4.getTitle()))); } - + @Test void getExamsWithSubgroups () throws Exception { // given @@ -598,10 +596,10 @@ void getExamsWithSubgroups () throws Exception { Exam exam3 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex3", Set.of("12A2"))); Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04"))); Exam exam5 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex5", Set.of("11K", "L04"))); - + // when MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("11K2"), Set.of("L04", "P04", "K04")); - + // then JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString()); assertEquals(2, responseArray.size()); @@ -611,7 +609,7 @@ void getExamsWithSubgroups () throws Exception { assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle()))); assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam4.getTitle()))); } - + @Test void getExamsWithSubgroupsUsingWholeYearIdentifier () throws Exception { // given @@ -621,10 +619,10 @@ void getExamsWithSubgroupsUsingWholeYearIdentifier () throws Exception { Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04"))); Exam exam5 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex5", Set.of("11K", "L04"))); Exam exam6 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex6", Set.of("12K", "L04", "P04"))); - + // when MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("12K"), Set.of("L04", "K04")); - + // then JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString()); assertEquals(2, responseArray.size()); @@ -635,7 +633,7 @@ void getExamsWithSubgroupsUsingWholeYearIdentifier () throws Exception { assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle()))); assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam5.getTitle()))); } - + @Test void getExamsMultipleGeneralGroupsAndSubgroups () throws Exception { // when @@ -647,7 +645,7 @@ void getExamsMultipleGeneralGroupsAndSubgroups () throws Exception { // then assertResponseMessage("Invalid group identifier: ambiguous general groups for subgroups", result); } - + @Test void getExamsWithSwappedGroupNames () throws Exception { // when @@ -659,7 +657,7 @@ void getExamsWithSwappedGroupNames () throws Exception { // then assertResponseMessage("Specified general group [K04] doesn't exists", result); } - + @Test void getExamsWithInvalidSubgroup () throws Exception { // when @@ -671,25 +669,25 @@ void getExamsWithInvalidSubgroup () throws Exception { // then assertResponseMessage("Specified sub group [11K2] doesn't exists", result); } - + // - + @Test void getExamTypesWhenExamTypesExists () throws Exception { // given ExamType exam = createExampleExamType("Exam"); ExamType project = createExampleExamType("Project"); - + // when MvcResult result = assertGetExamTypesRequest(status().isOk()); JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString()); - + // then assertEquals(2, responseArray.size()); assertTrue(responseArray.valueStream().anyMatch(e -> e.get("name").asText().equals(exam.getName()))); assertTrue(responseArray.valueStream().anyMatch(e -> e.get("name").asText().equals(project.getName()))); } - + @Test void getExamTypesWhenExamTypesNotExists () throws Exception { // given @@ -700,15 +698,15 @@ void getExamTypesWhenExamTypesNotExists () throws Exception { .andExpect(status().isOk()) .andReturn(); JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString()); - + // then assertEquals(0, responseArray.size()); } - + // - + // - + /** * this method create examType object and add it to repository * @@ -720,10 +718,9 @@ private ExamType createExampleExamType (String name) { examTypeRepository.save(examType); return examType; } - + /** * this method don't add created Exam to repository, because in that case id of created Exam would be unreachable - * * @param type ExamType object which is required argument of Exam * @return created Exam */ @@ -744,12 +741,12 @@ private Exam createExampleExam (ExamType type) { .examType(type) .build(); } - + private Exam createAndSaveExamWithTitleAndGroups (String title, Set groups) { ExamType examType = examTypeRepository .findByName("Project") .orElseGet(() -> createExampleExamType("Project")); - + Set groupsFromRepository = groupRepository .findAll() .stream() @@ -760,13 +757,13 @@ private Exam createAndSaveExamWithTitleAndGroups (String title, Set grou .filter(g -> !groupsFromRepository.contains(g)) .map(g -> StudentGroup.builder().name(g).build()) .collect(Collectors.toList())); - + Set groupsToSave = groupRepository .findAll() .stream() .filter(g -> groups.contains(g.getName())) .collect(Collectors.toSet()); - + return Exam .builder() .title(title) @@ -776,7 +773,7 @@ private Exam createAndSaveExamWithTitleAndGroups (String title, Set grou .examType(examType) .build(); } - + /** * @param examTypeName name of type of exam as String * @return created ExamDto @@ -792,7 +789,24 @@ private ExamDto createExampleExamDto (String examTypeName) { .subgroups(Set.of("L04")) .build(); } - + + /** + * @param examTypeName name of type of exam as String + * @param date . + * @return created ExamDto + */ + private ExamDto createExampleExamDto (String examTypeName, LocalDateTime date) { + return ExamDto + .builder() + .title("Math exam") + .description("first exam") + .date(date) + .examType(examTypeName) + .generalGroups(Set.of("12K2")) + .subgroups(Set.of("L04")) + .build(); + } + /** * compare error message form response with expected value * @@ -804,7 +818,7 @@ private void assertResponseMessage (String expectedMessage, MvcResult result) th assertTrue(jsonResponse.has("message")); assertEquals(expectedMessage, jsonResponse.get("message").asText()); } - + /** * method send POST request to ExamController with content as JSON attached to body and then check if response * code is the same as expected @@ -824,7 +838,7 @@ private MvcResult assertPostRequest (ResultMatcher expectedStatus, Object conten .andExpect(expectedStatus) .andReturn(); } - + /** * method send PUT request to ExamController with content as JSON attached to body and examId as pathID. * Then check if response code is the same as expected @@ -845,7 +859,7 @@ private MvcResult assertPutRequest (ResultMatcher expectedStatus, Object content .andExpect(expectedStatus) .andReturn(); } - + /** * method send DELETE request to ExamController with examId as pathID. * Then check if response code is the same as expected @@ -863,7 +877,7 @@ private MvcResult assertDeleteRequest (ResultMatcher expectedStatus, int pathId) .andExpect(expectedStatus) .andReturn(); } - + /** * method send GET request to ExamController at /pkwmtt/api/v1/exams/{id} URI with examId as pathID. * Then check if response code is the same as expected @@ -881,7 +895,7 @@ private MvcResult assertGetByIdRequest (ResultMatcher expectedStatus, int pathId .andExpect(expectedStatus) .andReturn(); } - + private MvcResult assertGetByGroupsRequest (ResultMatcher expectedStatus, Set generalGroups) throws Exception { return mockMvc @@ -893,7 +907,7 @@ private MvcResult assertGetByGroupsRequest (ResultMatcher expectedStatus, Set generalGroups, Set subgroups) throws Exception { return mockMvc @@ -906,7 +920,7 @@ private MvcResult assertGetByGroupsRequest (ResultMatcher expectedStatus, Set - + } \ No newline at end of file From e9cdab87e6d1892f7bdb10957b3dc42b8831f8a1 Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Wed, 10 Sep 2025 19:27:06 +0200 Subject: [PATCH 2/9] security for examCalendar --- .../org/pkwmtt/security/config/SpringSecurity.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java index e6e94fc..9b624af 100644 --- a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java +++ b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -22,7 +23,14 @@ public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { http .cors(withDefaults()) .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll().anyRequest().authenticated()) + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.POST , "/pkwmtt/api/v1/exams").authenticated() + .requestMatchers(HttpMethod.PUT , "/pkwmtt/api/v1/exams").authenticated() + .requestMatchers(HttpMethod.DELETE , "/pkwmtt/api/v1/exams").authenticated() + .requestMatchers(HttpMethod.GET , "/pkwmtt/api/v1/exams").permitAll() + .requestMatchers("/**").permitAll() + .anyRequest().authenticated() + ) .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); log.info("Configuring Success..."); return http.build(); From bbf3120f2a42d8e2708b580f4b00f5edb93c76e1 Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Wed, 10 Sep 2025 20:07:53 +0200 Subject: [PATCH 3/9] disable security for examController tests --- src/main/java/org/pkwmtt/security/config/SpringSecurity.java | 1 - src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java | 3 +++ src/test/resources/application-database.properties | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java index 9b624af..82b43fe 100644 --- a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java +++ b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java @@ -27,7 +27,6 @@ public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.POST , "/pkwmtt/api/v1/exams").authenticated() .requestMatchers(HttpMethod.PUT , "/pkwmtt/api/v1/exams").authenticated() .requestMatchers(HttpMethod.DELETE , "/pkwmtt/api/v1/exams").authenticated() - .requestMatchers(HttpMethod.GET , "/pkwmtt/api/v1/exams").permitAll() .requestMatchers("/**").permitAll() .anyRequest().authenticated() ) diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java index 5c8b9a8..438af4b 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java @@ -13,6 +13,7 @@ import org.pkwmtt.examCalendar.repository.ExamRepository; import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; +import org.pkwmtt.security.config.NoSecurityConfig; import org.pkwmtt.timetable.TimetableService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; @@ -20,6 +21,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; @@ -47,6 +49,7 @@ @AutoConfigureMockMvc @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) @ActiveProfiles("database") +@ContextConfiguration(classes = NoSecurityConfig.class) class ExamControllerTest { @Autowired diff --git a/src/test/resources/application-database.properties b/src/test/resources/application-database.properties index faff684..45c6df0 100644 --- a/src/test/resources/application-database.properties +++ b/src/test/resources/application-database.properties @@ -16,4 +16,7 @@ spring.mail.password=test spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=false +#allow overriding SecurityFilterChain for disabling security for tests +spring.main.allow-bean-definition-overriding=true + From fd1df3dd65fc52fb4ead9ffdf8f5d71d594b3ac3 Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Tue, 16 Sep 2025 16:10:40 +0200 Subject: [PATCH 4/9] Authorization for add exam --- .../org/pkwmtt/examCalendar/ExamService.java | 19 +++++++++++++++++++ .../repository/UserRepository.java | 5 +++++ .../security/config/SpringSecurity.java | 9 ++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java index 3bd6bb7..0b74370 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamService.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -11,8 +11,12 @@ import org.pkwmtt.examCalendar.repository.ExamRepository; import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; +import org.pkwmtt.examCalendar.repository.UserRepository; import org.pkwmtt.exceptions.*; import org.pkwmtt.timetable.TimetableService; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.*; @@ -26,6 +30,7 @@ public class ExamService { private final ExamRepository examRepository; private final ExamTypeRepository examTypeRepository; private final GroupRepository groupRepository; + private final UserRepository userRepository; private final TimetableService timetableService; /** @@ -34,6 +39,8 @@ public class ExamService { */ public int addExam(ExamDto examDto) { + verifyGroupPermissions(examDto.getGeneralGroups()); + Set groups = verifyAndUpdateExamGroups(examDto); ExamType examType = examTypeRepository.findByName(examDto.getExamType()) @@ -261,4 +268,16 @@ private static void verifySubgroupsFormat(Set subgroups) throws Specifie throw new SpecifiedSubGroupDoesntExistsException(group); }); } + + /** + * verifies if user had authorities to perform action for specific groups + * @param groups set of provided groups + */ + private void verifyGroupPermissions(Set groups){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userEmail = (String) authentication.getPrincipal(); + String userGroup = userRepository.findGroupByUserEmail(userEmail).orElseThrow(() -> new AccessDeniedException("There are no group assigned to user")); + if(!trimLastDigit(groups).equals(userGroup)) + throw new AccessDeniedException("You don't have permission to access this group"); + } } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java index 3a195fa..264c167 100644 --- a/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java @@ -3,6 +3,8 @@ import org.pkwmtt.examCalendar.entity.GeneralGroup; import org.pkwmtt.examCalendar.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -10,4 +12,7 @@ public interface UserRepository extends JpaRepository { Optional findByEmail (String email); Optional findByGeneralGroup (GeneralGroup generalGroup); + + @Query("SELECT g.name FROM User u LEFT JOIN u.generalGroup g where u.email = :email") + Optional findGroupByUserEmail (@Param("email") String userEmail); } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java index 82b43fe..0000b62 100644 --- a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java +++ b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java @@ -1,6 +1,8 @@ package org.pkwmtt.security.config; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.pkwmtt.security.token.filter.JwtFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -8,6 +10,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; @@ -15,7 +18,10 @@ @EnableWebSecurity @Slf4j @Configuration +@RequiredArgsConstructor public class SpringSecurity { + + private final JwtFilter jwtFilter; @Bean public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { @@ -30,7 +36,8 @@ public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { .requestMatchers("/**").permitAll() .anyRequest().authenticated() ) - .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)); + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); log.info("Configuring Success..."); return http.build(); } From a992ecaf0d8ee85e9494f02b649fbe0ea8eb526f Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Tue, 16 Sep 2025 16:54:38 +0200 Subject: [PATCH 5/9] Authorization for examCalendar --- .../org/pkwmtt/examCalendar/ExamService.java | 50 ++++++++++++++++--- .../repository/ExamRepository.java | 2 + 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java index 0b74370..9af1716 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamService.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -39,7 +39,7 @@ public class ExamService { */ public int addExam(ExamDto examDto) { - verifyGroupPermissions(examDto.getGeneralGroups()); + verifyGroupPermissionsForNewResource(examDto.getGeneralGroups()); Set groups = verifyAndUpdateExamGroups(examDto); @@ -62,6 +62,8 @@ public void modifyExam(ExamDto examDto, int id) { examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); + verifyGroupPermissionsForModifiedResource(examDto.getGeneralGroups(), id); + Set groups = verifyAndUpdateExamGroups(examDto); ExamType examType = examTypeRepository.findByName(examDto.getExamType()) @@ -75,6 +77,7 @@ public void modifyExam(ExamDto examDto, int id) { */ public void deleteExam(int id) { examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); + verifyGroupPermissionsForExistingResource(id); examRepository.deleteById(id); } @@ -270,14 +273,47 @@ private static void verifySubgroupsFormat(Set subgroups) throws Specifie } /** - * verifies if user had authorities to perform action for specific groups - * @param groups set of provided groups + * verifies if user has authorities to add new resource + * @param newGroups set of provided groups */ - private void verifyGroupPermissions(Set groups){ + private void verifyGroupPermissionsForNewResource(Set newGroups){ + String userGroup = getUserGroup(); + if(!trimLastDigit(newGroups).equals(userGroup)) + throw new AccessDeniedException("You don't have permission to access this group"); + } + + /** + * verifies if user has authorities to modify existing resource + * @param examId id of existing resource + */ + private void verifyGroupPermissionsForExistingResource(Integer examId){ + String userGroup = getUserGroup(); + Set generalGroupsOfExam = examRepository.findGroupsByExamId(examId) + .stream() + .filter(group -> group.matches("^\\d.*")) + .collect(Collectors.toSet()); + if(!trimLastDigit(generalGroupsOfExam).equals(userGroup)) + throw new AccessDeniedException("You don't have permission to access this group"); + } + + /** + * verifies if user had authorities to replace existing resource with new one + * @param newGroups set of groups of new resource + * @param examId id of existing resource + */ + private void verifyGroupPermissionsForModifiedResource(Set newGroups, Integer examId){ + verifyGroupPermissionsForNewResource(newGroups); + verifyGroupPermissionsForExistingResource(examId); + } + + /** + * @return superior group identifier (e.g. 12K) of currently authenticated user + * @throws AccessDeniedException when user doesn't have assigned group + */ + private String getUserGroup() throws AccessDeniedException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userEmail = (String) authentication.getPrincipal(); - String userGroup = userRepository.findGroupByUserEmail(userEmail).orElseThrow(() -> new AccessDeniedException("There are no group assigned to user")); - if(!trimLastDigit(groups).equals(userGroup)) - throw new AccessDeniedException("You don't have permission to access this group"); + return userRepository.findGroupByUserEmail(userEmail) + .orElseThrow(() -> new AccessDeniedException("There are no group assigned to user")); } } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java index eec6202..e89d873 100644 --- a/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java @@ -11,6 +11,8 @@ public interface ExamRepository extends JpaRepository { Set findAllByTitle(String title); + @Query("SELECT g.name FROM Exam e LEFT JOIN e.groups g WHERE e.examId = :id") + Set findGroupsByExamId(@Param("id") Integer examId); /** * @param groups set of generalGroups From 1d2edde45d6b09c20283c7d814cb616aa6c5c4fe Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Tue, 16 Sep 2025 19:28:58 +0200 Subject: [PATCH 6/9] change verification from repository to extra claims --- .../org/pkwmtt/examCalendar/ExamService.java | 17 ++++++------- .../token/JwtAuthenticationToken.java | 24 +++++++++++++++++++ .../org/pkwmtt/security/token/JwtService.java | 4 +++- .../pkwmtt/security/token/JwtServiceImpl.java | 2 +- .../security/token/filter/JwtFilter.java | 7 +++--- 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/pkwmtt/security/token/JwtAuthenticationToken.java diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java index 9af1716..fb636c9 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamService.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -11,15 +11,16 @@ import org.pkwmtt.examCalendar.repository.ExamRepository; import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; -import org.pkwmtt.examCalendar.repository.UserRepository; import org.pkwmtt.exceptions.*; +import org.pkwmtt.security.token.JwtAuthenticationToken; import org.pkwmtt.timetable.TimetableService; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -30,7 +31,6 @@ public class ExamService { private final ExamRepository examRepository; private final ExamTypeRepository examTypeRepository; private final GroupRepository groupRepository; - private final UserRepository userRepository; private final TimetableService timetableService; /** @@ -311,9 +311,10 @@ private void verifyGroupPermissionsForModifiedResource(Set newGroups, In * @throws AccessDeniedException when user doesn't have assigned group */ private String getUserGroup() throws AccessDeniedException { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String userEmail = (String) authentication.getPrincipal(); - return userRepository.findGroupByUserEmail(userEmail) - .orElseThrow(() -> new AccessDeniedException("There are no group assigned to user")); + JwtAuthenticationToken authentication = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + String group = authentication.getExamGroup(); + if(group == null) + throw new AccessDeniedException("You doesn't have access to any group"); + return group; } } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/security/token/JwtAuthenticationToken.java b/src/main/java/org/pkwmtt/security/token/JwtAuthenticationToken.java new file mode 100644 index 0000000..1bdb61f --- /dev/null +++ b/src/main/java/org/pkwmtt/security/token/JwtAuthenticationToken.java @@ -0,0 +1,24 @@ +package org.pkwmtt.security.token; + +import lombok.Getter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken { + + @Getter + private String examGroup; + + + public JwtAuthenticationToken(Object principal, Collection authorities) { + super(principal, null, authorities); + } + + public JwtAuthenticationToken(Object principal, Collection authorities, String group) { + super(principal, null, authorities); + this.examGroup = group; + } + +} diff --git a/src/main/java/org/pkwmtt/security/token/JwtService.java b/src/main/java/org/pkwmtt/security/token/JwtService.java index e62bc7b..cbca24c 100644 --- a/src/main/java/org/pkwmtt/security/token/JwtService.java +++ b/src/main/java/org/pkwmtt/security/token/JwtService.java @@ -1,12 +1,14 @@ package org.pkwmtt.security.token; +import io.jsonwebtoken.Claims; import org.pkwmtt.examCalendar.entity.User; import org.pkwmtt.security.token.dto.UserDTO; -import java.util.Optional; +import java.util.function.Function; public interface JwtService { String generateToken(UserDTO user); Boolean validateToken(String token, User user); String getUserEmailFromToken(String token); + T extractClaim(String token, Function claimResolver); } diff --git a/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java b/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java index 0e2858d..5946cf5 100644 --- a/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java +++ b/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java @@ -109,7 +109,7 @@ private boolean isTokenExpired(String token){ * @param claimResolver function to extract the desired claim from Claims * @return the extracted claim of type T */ - T extractClaim(String token, Function claimResolver) { + public T extractClaim(String token, Function claimResolver) { final Claims claims = extractAllClaims(token); return claimResolver.apply(claims); } diff --git a/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java b/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java index f5d6749..62f065c 100644 --- a/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java +++ b/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.pkwmtt.examCalendar.entity.User; import org.pkwmtt.examCalendar.repository.UserRepository; +import org.pkwmtt.security.token.JwtAuthenticationToken; import org.pkwmtt.security.token.JwtService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -65,10 +66,10 @@ protected void doFilterInternal(HttpServletRequest request, ); UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( + new JwtAuthenticationToken( user.getEmail(), - null, - authorities + authorities, + jwtService.extractClaim(token, claims -> claims.get("group", String.class)) ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); From a4dc7bb18753acdc3fa891bb090c976ab444a666 Mon Sep 17 00:00:00 2001 From: PatMaz999 Date: Tue, 16 Sep 2025 20:22:01 +0200 Subject: [PATCH 7/9] adjust tests for security implementation --- .../org/pkwmtt/examCalendar/ExamService.java | 2 ++ .../examCalendar/ExamControllerTest.java | 19 +++++++++++++++++ .../pkwmtt/examCalendar/ExamServiceTest.java | 21 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java index fb636c9..fa4ee54 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamService.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -220,6 +220,8 @@ private static String trimLastDigit(String generalGroup) { * @throws InvalidGroupIdentifierException when not all provided groups belong to the same year of study */ private static String trimLastDigit(Set superiorGroups) throws InvalidGroupIdentifierException { + if(superiorGroups == null || superiorGroups.isEmpty()) + throw new InvalidGroupIdentifierException("general group is missing"); Set trimmedGroups = superiorGroups.stream() .map(ExamService::trimLastDigit) .collect(Collectors.toSet()); diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java index 438af4b..68f0c14 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.transaction.Transactional; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -14,12 +15,14 @@ import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; import org.pkwmtt.security.config.NoSecurityConfig; +import org.pkwmtt.security.token.JwtAuthenticationToken; import org.pkwmtt.timetable.TimetableService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -29,6 +32,7 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -77,6 +81,21 @@ void setupBeforeEach () { groupRepository.deleteAll(); } + @BeforeEach + void setupSecurityContext() { + JwtAuthenticationToken auth = new JwtAuthenticationToken( + "user@example.com", + Collections.emptyList(), + "12K" + ); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + // /** diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java index f2c2fff..d793b92 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -17,7 +18,9 @@ import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; import org.pkwmtt.exceptions.*; +import org.pkwmtt.security.token.JwtAuthenticationToken; import org.pkwmtt.timetable.TimetableService; +import org.springframework.security.core.context.SecurityContextHolder; import java.time.LocalDateTime; import java.util.*; @@ -46,6 +49,17 @@ class ExamServiceTest { @InjectMocks private ExamService examService; + @BeforeEach + void setupSecurityContextHolder(){ + JwtAuthenticationToken token = new JwtAuthenticationToken( + "user@example.com", + Collections.emptyList(), + "12K" + ); + + SecurityContextHolder.getContext().setAuthentication(token); + } + // /** @@ -213,7 +227,10 @@ void addExamForEmptyGeneralGroup() { Set subgroups = Set.of("K04"); LocalDateTime date = LocalDateTime.now().plusDays(1); ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date); - RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto)); + RuntimeException exception = assertThrows( + InvalidGroupIdentifierException.class, + () -> examService.addExam(examDto) + ); assertEquals("Invalid group identifier: general group is missing", exception.getMessage()); } @@ -228,6 +245,7 @@ void addExamThatAlreadyExists() throws JsonProcessingException { when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K1", "12K2", "12K3"))); when(timetableService.getAvailableSubGroups("12K2")).thenReturn(new ArrayList<>(List.of("L04"))); + //noinspection unchecked when(groupRepository.findAllByNameIn(any(Set.class))).thenReturn(studentGroups); // @@ -629,6 +647,7 @@ void shouldDeleteExamWhenIdExists() { // given int examId = 1; when(examRepository.findById(examId)).thenReturn(Optional.of(mock(Exam.class))); + when(examRepository.findGroupsByExamId(examId)).thenReturn(Set.of("12K2")); // when examService.deleteExam(examId); // then From 7c0665970f86c49e818e352a43250d127a07424e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florczak?= <84631301+florczaq@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:18:07 +0200 Subject: [PATCH 8/9] Dockerfile: COPY .env only if exists --- .env.example | 0 Dockerfile | 1 + 2 files changed, 1 insertion(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile index f2676ca..cec75b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ COPY src ./src RUN mvn clean package -DskipTests FROM amazoncorretto:21 WORKDIR /app +COPY .env* . COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] From 44f6ef93395f5927770a439833683eb24252c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florczak?= <84631301+florczaq@users.noreply.github.com> Date: Wed, 17 Sep 2025 20:26:58 +0200 Subject: [PATCH 9/9] Otp: override code when generated for same group --- .../pkwmtt/examCalendar/ExamController.java | 4 ++++ .../repository/UserRepository.java | 4 ++++ .../config/SwaggerEndpointConfiguration.java | 18 ++++++++++----- .../java/org/pkwmtt/otp/OTPController.java | 2 +- .../org/pkwmtt/otp/OTPExceptionHandler.java | 2 +- src/main/java/org/pkwmtt/otp/OTPService.java | 23 +++++++++++-------- .../otp/repository/OTPCodeRepository.java | 3 +++ 7 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamController.java b/src/main/java/org/pkwmtt/examCalendar/ExamController.java index 8e7ebd0..a0d9564 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamController.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamController.java @@ -1,5 +1,6 @@ package org.pkwmtt.examCalendar; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; @@ -29,6 +30,7 @@ public class ExamController { * @return 201 created with URI to GET method which returns created resource */ @PostMapping("") + @SecurityRequirement(name = "bearerAuth") public ResponseEntity addExam(@RequestBody @Valid ExamDto examDto){ int id = examService.addExam(examDto); URI uri = ServletUriComponentsBuilder @@ -45,6 +47,7 @@ public ResponseEntity addExam(@RequestBody @Valid ExamDto examDto){ * @return 204 no content */ @PutMapping("/{id}") + @SecurityRequirement(name = "bearerAuth") public ResponseEntity modifyExam(@PathVariable @Positive int id, @RequestBody @Valid ExamDto examDto) { examService.modifyExam(examDto, id); return ResponseEntity.noContent().build(); @@ -55,6 +58,7 @@ public ResponseEntity modifyExam(@PathVariable @Positive int id, @RequestB * @return 204 no content */ @DeleteMapping("/{id}") + @SecurityRequirement(name = "bearerAuth") public ResponseEntity deleteExam(@PathVariable int id) { examService.deleteExam(id); return ResponseEntity.noContent().build(); diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java index 264c167..9992798 100644 --- a/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java @@ -1,5 +1,6 @@ package org.pkwmtt.examCalendar.repository; +import jakarta.transaction.Transactional; import org.pkwmtt.examCalendar.entity.GeneralGroup; import org.pkwmtt.examCalendar.entity.User; import org.springframework.data.jpa.repository.JpaRepository; @@ -15,4 +16,7 @@ public interface UserRepository extends JpaRepository { @Query("SELECT g.name FROM User u LEFT JOIN u.generalGroup g where u.email = :email") Optional findGroupByUserEmail (@Param("email") String userEmail); + + @Transactional + void deleteUserByEmail (String email); } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/global/config/SwaggerEndpointConfiguration.java b/src/main/java/org/pkwmtt/global/config/SwaggerEndpointConfiguration.java index f7f166b..ef0ad9b 100644 --- a/src/main/java/org/pkwmtt/global/config/SwaggerEndpointConfiguration.java +++ b/src/main/java/org/pkwmtt/global/config/SwaggerEndpointConfiguration.java @@ -40,7 +40,7 @@ public GroupedOpenApi publicEndpointCustomizer () { .pathsToMatch(apiPrefix + "/**", "/admin/**").addOpenApiCustomizer(openApi -> { Paths paths = openApi.getPaths(); - paths.forEach((path, pathItem) -> pathItem.readOperations().forEach(operation -> { + paths.forEach((path, pathItem) -> pathItem.readOperationsMap().forEach(((httpMethod, operation) -> { if (path.startsWith("/admin")) { addHeaderIfMissing( operation, @@ -48,7 +48,8 @@ public GroupedOpenApi publicEndpointCustomizer () { "Admin API key", "Admin-only endpoint", "Requires X-ADMIN-KEY header", - "admin" + "admin", + true ); } else if (path.startsWith(apiPrefix)) { addHeaderIfMissing( @@ -57,21 +58,26 @@ public GroupedOpenApi publicEndpointCustomizer () { "Your API key", "Public API endpoint", "Requires X-API-KEY header", - "public" + "public", + true ); } - })); +// if (path.contains("exams") && (httpMethod.equals(PathItem.HttpMethod.POST) || httpMethod.equals( +// PathItem.HttpMethod.PUT) || httpMethod.equals(PathItem.HttpMethod.DELETE))) { +// operation.addSecurityItem(new SecurityRequirement().addList("bearerAuth")); +// } + }))); }).build(); } - private void addHeaderIfMissing (Operation operation, String headerName, String headerDescription, String summary, String description, String tag) { + private void addHeaderIfMissing (Operation operation, String headerName, String headerDescription, String summary, String description, String tag, boolean required) { operation.setSummary(summary); operation.setDescription(description); operation.addTagsItem(tag); operation.addParametersItem(new Parameter() .name(headerName) .in("header") - .required(true) + .required(required) .description(headerDescription) .schema(new StringSchema())); } diff --git a/src/main/java/org/pkwmtt/otp/OTPController.java b/src/main/java/org/pkwmtt/otp/OTPController.java index 284fa21..22ddc1b 100644 --- a/src/main/java/org/pkwmtt/otp/OTPController.java +++ b/src/main/java/org/pkwmtt/otp/OTPController.java @@ -24,7 +24,7 @@ public ResponseEntity authenticate (@RequestParam(name = "c") String cod @PostMapping("/codes/generate") public ResponseEntity generateCodes (@RequestBody List request) - throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedGeneralGroupDoesntExistsException { + throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedGeneralGroupDoesntExistsException, IllegalArgumentException { service.sendOTPCodesForManyGroups(request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/pkwmtt/otp/OTPExceptionHandler.java b/src/main/java/org/pkwmtt/otp/OTPExceptionHandler.java index 0dc2849..1a4dfe7 100644 --- a/src/main/java/org/pkwmtt/otp/OTPExceptionHandler.java +++ b/src/main/java/org/pkwmtt/otp/OTPExceptionHandler.java @@ -11,7 +11,7 @@ @RestControllerAdvice(assignableTypes = {OTPController.class}) public class OTPExceptionHandler { - @ExceptionHandler({OTPCodeNotFoundException.class, WrongOTPFormatException.class, UserNotFoundException.class, WrongArgumentException.class, SpecifiedGeneralGroupDoesntExistsException.class}) + @ExceptionHandler({OTPCodeNotFoundException.class, WrongOTPFormatException.class, UserNotFoundException.class, WrongArgumentException.class, SpecifiedGeneralGroupDoesntExistsException.class, IllegalArgumentException.class}) public ResponseEntity handleBadRequests (Exception e) { return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/org/pkwmtt/otp/OTPService.java b/src/main/java/org/pkwmtt/otp/OTPService.java index 24d55c0..52e2554 100644 --- a/src/main/java/org/pkwmtt/otp/OTPService.java +++ b/src/main/java/org/pkwmtt/otp/OTPService.java @@ -54,34 +54,35 @@ public String generateTokenForRepresentative (String code) } public void sendOTPCodesForManyGroups (List requests) - throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedSubGroupDoesntExistsException { + throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedSubGroupDoesntExistsException, IllegalArgumentException { requests.forEach(request -> { var code = generateNewCode(); var mail = createMail(request, code); var groupName = request.getGeneralGroupName(); var groupNameLength = groupName.length(); - if (groupNameLength > 3 && Character.isDigit(groupName.charAt(groupNameLength - 1))) { + if (groupNameLength > 3 && Character.isDigit(groupName.charAt(groupNameLength - 1))) { //Check general group name throw new WrongArgumentException( "Wrong general group provided. Make sure you are not providing subgroup. (f.e 12K1 -> wrong, 12K -> good)"); } - if (!generalGroupExists(groupName)) { + if (!generalGroupExists(groupName)) { // Check if general group with provided name exists throw new SpecifiedGeneralGroupDoesntExistsException(); } var generalGroup = generalGroupRepository.findByName(groupName); - if (generalGroup.isPresent()) { - if (otpRepository.existsOTPCodeByGeneralGroup(generalGroup.get())) { - throw new RuntimeException(""); + if (generalGroup.isPresent()) { //Check if general group is already saved in database + if (otpRepository.existsOTPCodeByGeneralGroup(generalGroup.get())) { //Check if provided general group has assigned code + otpRepository.deleteByGeneralGroup(generalGroup.get()); // Delete existing code } } else { + //Save general group to database generalGroup = Optional.of(generalGroupRepository.save(new GeneralGroup(null, groupName))); } try { - emailService.send(mail); + emailService.send(mail); //Send email } catch (MessagingException e) { throw new MailCouldNotBeSendException("Couldn't send mail for group: " + groupName); } @@ -94,13 +95,17 @@ public void sendOTPCodesForManyGroups (List requests) .isActive(true) .build(); - userRepository.save(user); + userRepository + .findByGeneralGroup(generalGroup.get()) + .ifPresent(value -> userRepository.deleteUserByEmail(value.getEmail())); + userRepository.save(user); otpRepository.save(new OTPCode(code, generalGroup.get())); }); } - private GeneralGroup getGeneralGroupAssignedToCode (String code) throws OTPCodeNotFoundException, WrongOTPFormatException { + private GeneralGroup getGeneralGroupAssignedToCode (String code) + throws OTPCodeNotFoundException, WrongOTPFormatException { this.validateCode(code); Optional result = otpRepository.findByCode(code); diff --git a/src/main/java/org/pkwmtt/otp/repository/OTPCodeRepository.java b/src/main/java/org/pkwmtt/otp/repository/OTPCodeRepository.java index 6fca6a6..5f74dd7 100644 --- a/src/main/java/org/pkwmtt/otp/repository/OTPCodeRepository.java +++ b/src/main/java/org/pkwmtt/otp/repository/OTPCodeRepository.java @@ -16,4 +16,7 @@ public interface OTPCodeRepository extends JpaRepository { boolean existsOTPCodeByGeneralGroup (GeneralGroup generalGroup); boolean existsOTPCodeByCode (String code); + + @Transactional + void deleteByGeneralGroup (GeneralGroup generalGroup); } \ No newline at end of file