diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 9ccf3e8..ea94ec7 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -40,6 +40,8 @@ jobs: SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }} SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} SPRING_PROFILES_ACTIVE: ${{ secrets.SPRING_PROFILES_ACTIVE }} + EMAIL_USERNAME: ${{ secrets.EMAIL_USERNAME }} + EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} steps: - name: Checkout code @@ -108,3 +110,6 @@ jobs: ghcr.io/${{ env.IMAGE_REPO }}:${{ github.run_number }} cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + EMAIL_USERNAME=${{ secrets.EMAIL_USERNAME }} + EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }} \ No newline at end of file diff --git a/README.md b/README.md index bbbb10e..1ea4873 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,10 @@ ### 1. Clone the repository ```shell -docker pull ghcr.io/pkttteam/pkwmtt-backend:[PACKAGE_NUMBER] +docker pull ghcr.io/pkttteam/pkwmtt-backend:latest ``` -### 2. Create a `.env` file - -Create `.env` file in project - -Update values like: - -```dotenv -MYSQL_ROOT_PASSWORD=example -MYSQL_DATABASE=example_db -MYSQL_USER=username -MYSQL_PASSWORD=password - -SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/example_db -SPRING_DATASOURCE_USERNAME=username -SPRING_DATASOURCE_PASSWORD=password -``` - -### 3. Run +### 2. Run ```shell docker run -d --name [image_name] -p 8080:8080 ghcr.io/pkttteam/pkwmttt-backend:[PACKAGE_NUMBER] diff --git a/init.sql b/init.sql index df5e8a1..7d91d29 100644 --- a/init.sql +++ b/init.sql @@ -3,7 +3,7 @@ -- https://www.phpmyadmin.net/ -- -- Host: db --- Generation Time: Lip 31, 2025 at 06:56 PM +-- Generation Time: Aug 18, 2025 at 07:00 PM -- Wersja serwera: 9.3.0 -- Wersja PHP: 8.2.27 @@ -32,10 +32,9 @@ USE `pktt`; DROP TABLE IF EXISTS `exams`; CREATE TABLE `exams` ( `exam_id` int NOT NULL, - `title` varchar(255) DEFAULT NULL, + `title` varchar(255) NOT NULL, `description` varchar(255) DEFAULT NULL, - `date` datetime(6) DEFAULT NULL, - `groups` varchar(255) DEFAULT NULL, + `exam_date` datetime NOT NULL, `exam_type_id` int NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; @@ -48,10 +47,51 @@ TRUNCATE TABLE `exams`; -- Zrzut danych tabeli `exams` -- -INSERT INTO `exams` (`exam_id`, `title`, `description`, `date`, `groups`, `exam_type_id`) VALUES -(1, 'Matematyka Dyskretna', 'Egzamin końcowy z matematyki dyskretnej', '2025-07-30 00:00:00.000000', '12K3,11L1', 2), -(2, 'Programowanie C++', 'Kolokwium z programowania w C++', '2025-08-05 00:00:00.000000', '12K2,13S3', 1), -(3, 'Sieci Komputerowe', 'Projekt zespołowy na sieciach komputerowych', '2025-09-10 00:00:00.000000', '14S4,12K1', 3); +INSERT INTO `exams` (`exam_id`, `title`, `description`, `exam_date`, `exam_type_id`) VALUES +(1, 'Kolokwium z matematyki', 'Pierwsze kolokwium obejmujące rozdziały 1–3', '2025-10-01 10:00:00', 1), +(2, 'Egzamin końcowy z programowania', 'Egzamin pisemny i praktyczny', '2025-01-20 09:00:00', 2), +(3, 'Projekt z baz danych', 'Oddanie projektu grupowego', '2025-06-15 23:59:00', 3), +(4, 'Kolokwium z fizyki', 'Druga część materiału: mechanika', '2025-11-05 12:00:00', 1), +(5, 'Egzamin końcowy z ekonomii', 'Egzamin pisemny testowy', '2025-02-10 08:30:00', 2), +(6, 'Projekt z systemów operacyjnych', 'Prezentacja projektu semestralnego', '2025-06-25 14:00:00', 3); + +-- -------------------------------------------------------- + +-- +-- Struktura tabeli dla tabeli `exams_groups` +-- + +DROP TABLE IF EXISTS `exams_groups`; +CREATE TABLE `exams_groups` ( + `exam_group_id` int NOT NULL, + `exam_id` int NOT NULL, + `group_id` int NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Tabela Truncate przed wstawieniem `exams_groups` +-- + +TRUNCATE TABLE `exams_groups`; +-- +-- Zrzut danych tabeli `exams_groups` +-- + +INSERT INTO `exams_groups` (`exam_group_id`, `exam_id`, `group_id`) VALUES +(7, 1, 9), +(8, 1, 10), +(9, 2, 12), +(10, 2, 13), +(11, 2, 14), +(12, 3, 15), +(13, 3, 16), +(14, 3, 17), +(15, 4, 9), +(16, 4, 10), +(17, 5, 12), +(18, 5, 13), +(19, 6, 15), +(20, 6, 16); -- -------------------------------------------------------- @@ -62,7 +102,7 @@ INSERT INTO `exams` (`exam_id`, `title`, `description`, `date`, `groups`, `exam_ DROP TABLE IF EXISTS `exam_type`; CREATE TABLE `exam_type` ( `exam_type_id` int NOT NULL, - `name` varchar(255) DEFAULT NULL + `name` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- @@ -88,7 +128,7 @@ INSERT INTO `exam_type` (`exam_type_id`, `name`) VALUES DROP TABLE IF EXISTS `general_group`; CREATE TABLE `general_group` ( `general_group_id` int NOT NULL, - `name` varchar(255) DEFAULT NULL + `name` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- @@ -101,10 +141,10 @@ TRUNCATE TABLE `general_group`; -- INSERT INTO `general_group` (`general_group_id`, `name`) VALUES -(11, '1'), -(12, '2'), -(13, '3'), -(14, '4'); +(17, '11A'), +(18, '12E'), +(19, '13K'), +(20, '14M'); -- -------------------------------------------------------- @@ -115,10 +155,7 @@ INSERT INTO `general_group` (`general_group_id`, `name`) VALUES DROP TABLE IF EXISTS `groups`; CREATE TABLE `groups` ( `group_id` int NOT NULL, - `letter` char(1) NOT NULL, - `group_count` int NOT NULL, - `general_group_id` int NOT NULL, - `name` varchar(255) DEFAULT NULL + `name` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- @@ -130,15 +167,16 @@ TRUNCATE TABLE `groups`; -- Zrzut danych tabeli `groups` -- -INSERT INTO `groups` (`group_id`, `letter`, `group_count`, `general_group_id`, `name`) VALUES -(1, 'K', 1, 11, NULL), -(2, 'K', 2, 12, NULL), -(3, 'L', 1, 11, NULL), -(4, 'L', 2, 12, NULL), -(5, 'S', 3, 13, NULL), -(6, 'S', 4, 14, NULL), -(7, 'K', 3, 12, NULL), -(8, 'L', 4, 14, NULL); +INSERT INTO `groups` (`group_id`, `name`) VALUES +(9, '11A1'), +(10, '11A2'), +(12, '12E1'), +(13, '12E2'), +(14, '12E3'), +(15, '13K1'), +(16, '13K2'), +(17, '13K3'), +(18, '14M1'); -- -------------------------------------------------------- @@ -148,12 +186,10 @@ INSERT INTO `groups` (`group_id`, `letter`, `group_count`, `general_group_id`, ` DROP TABLE IF EXISTS `otp_codes`; CREATE TABLE `otp_codes` ( - `code` varchar(255) DEFAULT NULL, - `expire` timestamp NOT NULL, - `used` tinyint(1) NOT NULL, - `user_id` int NOT NULL, `otp_code_id` int NOT NULL, - `timestamp` datetime(6) DEFAULT NULL + `code` varchar(255) NOT NULL, + `expire` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `general_group_id` int NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- @@ -161,6 +197,16 @@ CREATE TABLE `otp_codes` ( -- TRUNCATE TABLE `otp_codes`; +-- +-- Zrzut danych tabeli `otp_codes` +-- + +INSERT INTO `otp_codes` (`otp_code_id`, `code`, `expire`, `general_group_id`) VALUES +(1, 'ABC123', '2025-08-18 19:51:40', 17), +(2, 'XYZ789', '2025-08-18 20:51:40', 18), +(3, 'QWE456', '2025-08-18 21:51:40', 19), +(4, 'JKL999', '2025-08-18 22:51:40', 20); + -- -------------------------------------------------------- -- @@ -171,9 +217,9 @@ DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `user_id` int NOT NULL, `general_group_id` int NOT NULL, - `email` varchar(254) NOT NULL, - `is_active` tinyint(1) NOT NULL, - `role` enum('ADMIN','REPRESENTATIVE') NOT NULL + `email` varchar(255) NOT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `role` enum('ADMIN','REPRESENTATIVE') NOT NULL DEFAULT 'REPRESENTATIVE' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- @@ -186,10 +232,10 @@ TRUNCATE TABLE `users`; -- INSERT INTO `users` (`user_id`, `general_group_id`, `email`, `is_active`, `role`) VALUES -(1, 12, 'jan.kowalski@example.com', 1, 'ADMIN'), -(2, 11, 'anna.nowak@example.com', 1, 'REPRESENTATIVE'), -(3, 13, 'piotr.zielinski@example.com', 0, 'REPRESENTATIVE'), -(4, 14, 'ewa.wisniewska@example.com', 1, 'ADMIN'); +(1, 17, 'user11a@example.com', 1, 'REPRESENTATIVE'), +(2, 18, 'user12e@example.com', 1, 'REPRESENTATIVE'), +(3, 19, 'user13k@example.com', 1, 'REPRESENTATIVE'), +(4, 20, 'user14m@example.com', 1, 'ADMIN'); -- -- Indeksy dla zrzutów tabel @@ -200,8 +246,15 @@ INSERT INTO `users` (`user_id`, `general_group_id`, `email`, `is_active`, `role` -- ALTER TABLE `exams` ADD PRIMARY KEY (`exam_id`), - ADD KEY `exam_type` (`exam_type_id`), - ADD KEY `exam_type_id` (`exam_type_id`); + ADD KEY `exam_type_id_idx` (`exam_type_id`); + +-- +-- Indeksy dla tabeli `exams_groups` +-- +ALTER TABLE `exams_groups` + ADD PRIMARY KEY (`exam_group_id`), + ADD KEY `exam_id_idx` (`exam_id`), + ADD KEY `group_id_idx` (`group_id`); -- -- Indeksy dla tabeli `exam_type` @@ -219,28 +272,38 @@ ALTER TABLE `general_group` -- Indeksy dla tabeli `groups` -- ALTER TABLE `groups` - ADD PRIMARY KEY (`group_id`), - ADD KEY `general_group` (`general_group_id`), - ADD KEY `general_group_id` (`general_group_id`); + ADD PRIMARY KEY (`group_id`); -- -- Indeksy dla tabeli `otp_codes` -- ALTER TABLE `otp_codes` ADD PRIMARY KEY (`otp_code_id`), - ADD KEY `user_id` (`user_id`); + ADD KEY `general_group_id_idx` (`general_group_id`); -- -- Indeksy dla tabeli `users` -- ALTER TABLE `users` ADD PRIMARY KEY (`user_id`), - ADD KEY `general_group_id` (`general_group_id`); + ADD KEY `general_group_id_idx` (`general_group_id`); -- -- AUTO_INCREMENT dla zrzuconych tabel -- +-- +-- AUTO_INCREMENT dla tabeli `exams` +-- +ALTER TABLE `exams` + MODIFY `exam_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; + +-- +-- AUTO_INCREMENT dla tabeli `exams_groups` +-- +ALTER TABLE `exams_groups` + MODIFY `exam_group_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21; + -- -- AUTO_INCREMENT dla tabeli `exam_type` -- @@ -251,13 +314,19 @@ ALTER TABLE `exam_type` -- AUTO_INCREMENT dla tabeli `general_group` -- ALTER TABLE `general_group` - MODIFY `general_group_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=15; + MODIFY `general_group_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21; -- -- AUTO_INCREMENT dla tabeli `groups` -- ALTER TABLE `groups` - MODIFY `group_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9; + MODIFY `group_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21; + +-- +-- AUTO_INCREMENT dla tabeli `otp_codes` +-- +ALTER TABLE `otp_codes` + MODIFY `otp_code_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5; -- -- AUTO_INCREMENT dla tabeli `users` @@ -273,25 +342,26 @@ ALTER TABLE `users` -- Ograniczenia dla tabeli `exams` -- ALTER TABLE `exams` - ADD CONSTRAINT `exams_ibfk_1` FOREIGN KEY (`exam_type_id`) REFERENCES `exam_type` (`exam_type_id`); + ADD CONSTRAINT `exams_ibfk_1` FOREIGN KEY (`exam_type_id`) REFERENCES `exam_type` (`exam_type_id`) ON DELETE CASCADE; -- --- Ograniczenia dla tabeli `groups` +-- Ograniczenia dla tabeli `exams_groups` -- -ALTER TABLE `groups` - ADD CONSTRAINT `groups_ibfk_1` FOREIGN KEY (`general_group_id`) REFERENCES `general_group` (`general_group_id`); +ALTER TABLE `exams_groups` + ADD CONSTRAINT `exams_groups_ibfk_1` FOREIGN KEY (`exam_id`) REFERENCES `exams` (`exam_id`) ON DELETE CASCADE, + ADD CONSTRAINT `exams_groups_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`group_id`) ON DELETE CASCADE; -- -- Ograniczenia dla tabeli `otp_codes` -- ALTER TABLE `otp_codes` - ADD CONSTRAINT `otp_codes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`); + ADD CONSTRAINT `otp_codes_ibfk_1` FOREIGN KEY (`general_group_id`) REFERENCES `general_group` (`general_group_id`) ON DELETE CASCADE; -- -- Ograniczenia dla tabeli `users` -- ALTER TABLE `users` - ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`general_group_id`) REFERENCES `general_group` (`general_group_id`); + ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`general_group_id`) REFERENCES `general_group` (`general_group_id`) ON DELETE CASCADE; COMMIT; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml index fd73cac..1e54dfb 100644 --- a/pom.xml +++ b/pom.xml @@ -60,11 +60,12 @@ - - com.h2database - h2 - runtime - + + + + + + @@ -77,22 +78,31 @@ test + junit junit 4.13.1 - - org.mockito - mockito-all - 1.10.19 - + + + + + + org.mockito mockito-core 5.18.0 + + + com.h2database + h2 + test + + org.jsoup @@ -132,8 +142,12 @@ org.springframework.boot spring-boot-starter-actuator - - + + org.springframework.boot + spring-boot-starter-validation + + + diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..bf26760 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,31 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "21" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:2025.1 diff --git a/src/main/java/org/pkwmtt/cache/CacheConfig.java b/src/main/java/org/pkwmtt/cache/CacheConfig.java index 97036fe..806a692 100644 --- a/src/main/java/org/pkwmtt/cache/CacheConfig.java +++ b/src/main/java/org/pkwmtt/cache/CacheConfig.java @@ -12,17 +12,19 @@ @Configuration @EnableCaching public class CacheConfig { + @Bean public Caffeine caffeineConfig() { return Caffeine.newBuilder() - .expireAfterWrite(24, TimeUnit.HOURS) + .expireAfterWrite(12, TimeUnit.HOURS) .recordStats(); } @Bean public CacheManager cacheManager(Caffeine caffeine) { - CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + CaffeineCacheManager cacheManager = new CaffeineCacheManager("timetables"); cacheManager.setCaffeine(caffeine); return cacheManager; } + } diff --git a/src/main/java/org/pkwmtt/config/HighlightingCompositeLogConverter.java b/src/main/java/org/pkwmtt/config/HighlightingCompositeLogConverter.java new file mode 100644 index 0000000..dc2440b --- /dev/null +++ b/src/main/java/org/pkwmtt/config/HighlightingCompositeLogConverter.java @@ -0,0 +1,19 @@ +package org.pkwmtt.config; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.color.ANSIConstants; +import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; + +public class HighlightingCompositeLogConverter extends ForegroundCompositeConverterBase { + + @Override + protected String getForegroundColorCode(ILoggingEvent event) { + return switch (event.getLevel().toInt()) { + case Level.ERROR_INT -> ANSIConstants.BOLD + ANSIConstants.RED_FG; + case Level.WARN_INT -> ANSIConstants.RED_FG; + case Level.INFO_INT -> ANSIConstants.CYAN_FG; + default -> ANSIConstants.DEFAULT_FG; + }; + } +} diff --git a/src/main/java/org/pkwmtt/entity/Exam.java b/src/main/java/org/pkwmtt/entity/Exam.java deleted file mode 100644 index 68d376c..0000000 --- a/src/main/java/org/pkwmtt/entity/Exam.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.pkwmtt.entity; - -import jakarta.persistence.*; -import lombok.*; - -import java.util.Date; - -@Entity -@Getter -@Builder -@RequiredArgsConstructor -@Table(name = "`exams`") -@AllArgsConstructor -public class Exam { - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Integer exam_id; - - private String title; - - private String description; - - private Date date; - - @Column(name = "`groups`") - private String exam_group; - - @ManyToOne - @JoinColumn(name = "exam_type_id") - private ExamType exam_type; -} diff --git a/src/main/java/org/pkwmtt/timetable/enums/SubjectType.java b/src/main/java/org/pkwmtt/enums/SubjectType.java similarity index 78% rename from src/main/java/org/pkwmtt/timetable/enums/SubjectType.java rename to src/main/java/org/pkwmtt/enums/SubjectType.java index bcb41af..f43282e 100644 --- a/src/main/java/org/pkwmtt/timetable/enums/SubjectType.java +++ b/src/main/java/org/pkwmtt/enums/SubjectType.java @@ -1,4 +1,4 @@ -package org.pkwmtt.timetable.enums; +package org.pkwmtt.enums; public enum SubjectType { LECTURE, diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamController.java b/src/main/java/org/pkwmtt/examCalendar/ExamController.java new file mode 100644 index 0000000..7c69cff --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/ExamController.java @@ -0,0 +1,90 @@ +package org.pkwmtt.examCalendar; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.Set; + +@Validated +@RequiredArgsConstructor +@RequestMapping("/pkwmtt/api/v1/exams") +@RestController +public class ExamController { + + private final ExamService examService; + + /** + * @param examDto details of exam + * @return 201 created with URI to GET method which returns created resource + */ + @PostMapping("") + public ResponseEntity addExam(@RequestBody @Valid ExamDto examDto) { + int id = examService.addExam(examDto); + URI uri = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(id) + .toUri(); + return ResponseEntity.created(uri).build(); +// TODO: test not null validation in controller + } + + /** + * @param id of exam or test + * @param examDto new details of exam or test + * @return 204 no content + */ + @PutMapping("/{id}") + public ResponseEntity modifyExam(@PathVariable @Positive int id, @RequestBody @Valid ExamDto examDto) { + examService.modifyExam(examDto, id); + return ResponseEntity.noContent().build(); + } + + /** + * @param id of exam or test + * @return 204 no content + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteExam(@PathVariable int id) { + examService.deleteExam(id); + return ResponseEntity.noContent().build(); + } + + /** + * @param id of exam or test + * @return 200 ok with single exam or test details + */ + @GetMapping("/{id}") + public ResponseEntity getExam(@PathVariable int id) { + return ResponseEntity.ok(examService.getExamById(id)); + } + + /** + * @param groups set of groups + * @return 200 ok with list of exams for specific group + */ + @GetMapping("/by-groups") + public ResponseEntity> getExams(@RequestParam Set groups){ + return ResponseEntity.ok(examService.getExamByGroup(groups)); + } + + /** + * @return 200 ok with list of available exam types + */ +// should be moved to new controller? + @GetMapping("/exam-types") + public ResponseEntity> getExamTypes(){ + return ResponseEntity.ok(examService.getExamTypes()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java b/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java new file mode 100644 index 0000000..845be7a --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java @@ -0,0 +1,48 @@ +package org.pkwmtt.examCalendar; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.pkwmtt.exceptions.ErrorResponseDTO; +import org.pkwmtt.exceptions.ExamTypeNotExistsException; +import org.pkwmtt.exceptions.NoSuchElementWithProvidedIdException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@RestControllerAdvice +public class ExamControllerAdvice { + +// TODO: handle or remove UnsupportedCountOfArgumentsException + + @ExceptionHandler(NoSuchElementWithProvidedIdException.class) + public ResponseEntity handleNoSuchElementWithProvidedIdException(NoSuchElementWithProvidedIdException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponseDTO(e.getMessage())); + } + + @ExceptionHandler(ExamTypeNotExistsException.class) + public ResponseEntity handleExamTypeNotExistsException(ExamTypeNotExistsException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponseDTO(e.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(field -> field.getField() + " : " + field.getDefaultMessage()) + .collect(Collectors.joining(", ")); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponseDTO(message)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + String message = e.getConstraintViolations().stream() + .map(field -> field.getPropertyPath() + " : " + field.getMessage()) + .collect(Collectors.joining(", ")); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponseDTO(message)); + } + + +} diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java new file mode 100644 index 0000000..8da2396 --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -0,0 +1,86 @@ +package org.pkwmtt.examCalendar; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.pkwmtt.examCalendar.mapper.ExamDtoToExamMapper; +import org.pkwmtt.examCalendar.repository.ExamRepository; +import org.pkwmtt.examCalendar.repository.ExamTypeRepository; +import org.pkwmtt.exceptions.NoSuchElementWithProvidedIdException; +import org.pkwmtt.exceptions.UnsupportedCountOfArgumentsException; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +@RequiredArgsConstructor +@Transactional +public class ExamService { + + private final ExamRepository examRepository; + private final ExamDtoToExamMapper examMapper; + private final ExamTypeRepository examTypeRepository; + + /** + * @param examDto details of exam + * @return id of exam added to database + */ + public int addExam(ExamDto examDto) { + return examRepository.save(examMapper.mapToNewExam(examDto)).getExamId(); + } + + /** + * @param examDto new details of exam that overwrite old ones + * @param id of exam that need to be modified + */ + public void modifyExam(ExamDto examDto, int id) { + examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); + examRepository.save(examMapper.mapToExistingExam(examDto, id)); + } + + /** + * @param id of exam + */ + public void deleteExam(int id) { + examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); + examRepository.deleteById(id); + } + + /** + * @param id of exam + * @return exam + */ + public Exam getExamById(int id) { + return examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); + } + + /** + * @param groups set od groups (max 4) + * @return set of exams for specific groups + */ + public Set getExamByGroup(Set groups) { + if (groups.size() > 4 || groups.isEmpty()) + throw new UnsupportedCountOfArgumentsException(1, 5, groups.size()); + List groupList = new ArrayList<>(groups); + return switch (groupList.size()) { + case 4 -> examRepository.findExamsByGroupsIdentifier( + groupList.get(0), groupList.get(1), groupList.get(2), groupList.get(3)); + case 3 -> examRepository.findExamsByGroupsIdentifier( + groupList.get(0), groupList.get(1), groupList.get(2)); + case 2 -> examRepository.findExamsByGroupsIdentifier( + groupList.get(0), groupList.get(1)); + case 1 -> examRepository.findExamsByGroupsIdentifier( + groupList.get(0)); + default -> Set.of(); + }; + } + + /** + * @return list of examTypes + */ + public List getExamTypes() { + return examTypeRepository.findAll(); + } +} diff --git a/src/main/java/org/pkwmtt/examCalendar/dto/ExamDto.java b/src/main/java/org/pkwmtt/examCalendar/dto/ExamDto.java new file mode 100644 index 0000000..f1cfdfb --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/dto/ExamDto.java @@ -0,0 +1,33 @@ +package org.pkwmtt.examCalendar.dto; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class ExamDto { + + @NotBlank + @Size(max = 255, message = "max size of field is 255") + private final String title; + + @Size(max = 255, message = "max size of field is 255") + private final String description; + + @Future(message = "Date must be in the future") + @NotNull + private final LocalDateTime date; + + @NotBlank + @Size(max = 255, message = "max size of field is 255") + private final String examGroups; + + @NotNull + private final String examType; +} diff --git a/src/main/java/org/pkwmtt/examCalendar/entity/Exam.java b/src/main/java/org/pkwmtt/examCalendar/entity/Exam.java new file mode 100644 index 0000000..89fe16d --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/entity/Exam.java @@ -0,0 +1,48 @@ +package org.pkwmtt.examCalendar.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.pkwmtt.exceptions.InvalidGroupIdentifierException; + +import java.time.LocalDateTime; +import java.util.Arrays; + +@Entity +@Getter +@Builder(builderClassName = "Builder", buildMethodName = "build") +@RequiredArgsConstructor +@Table(name = "exams") +@AllArgsConstructor +public class Exam { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer examId; + + private String title; + + private String description; + + private LocalDateTime date; + + @Column(name = "`groups`") + private String examGroups; + + @ManyToOne + @JoinColumn(name = "exam_type_id") + private ExamType examType; + + @SuppressWarnings("unused") + public static class Builder { + public Exam build() { + // max length of group identifier is 6 + Arrays.stream(examGroups.split(", ")).forEach(group -> { + if(group.length() > 6) + throw new InvalidGroupIdentifierException(group); + }); + return new Exam(examId, title, description, date, examGroups, examType); + } + } +} diff --git a/src/main/java/org/pkwmtt/entity/ExamType.java b/src/main/java/org/pkwmtt/examCalendar/entity/ExamType.java similarity index 57% rename from src/main/java/org/pkwmtt/entity/ExamType.java rename to src/main/java/org/pkwmtt/examCalendar/entity/ExamType.java index 9ec4a4f..6c5355f 100644 --- a/src/main/java/org/pkwmtt/entity/ExamType.java +++ b/src/main/java/org/pkwmtt/examCalendar/entity/ExamType.java @@ -1,23 +1,21 @@ -package org.pkwmtt.entity; +package org.pkwmtt.examCalendar.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Entity @Getter @Builder @AllArgsConstructor -@Table(name = "`exam_type`") +@RequiredArgsConstructor +@Table(name = "exam_type") public class ExamType { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer exam_type_id; private String name; - - public ExamType() { - - } } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/entity/GeneralGroup.java b/src/main/java/org/pkwmtt/examCalendar/entity/GeneralGroup.java similarity index 72% rename from src/main/java/org/pkwmtt/entity/GeneralGroup.java rename to src/main/java/org/pkwmtt/examCalendar/entity/GeneralGroup.java index f03ff42..3eb27af 100644 --- a/src/main/java/org/pkwmtt/entity/GeneralGroup.java +++ b/src/main/java/org/pkwmtt/examCalendar/entity/GeneralGroup.java @@ -1,9 +1,10 @@ -package org.pkwmtt.entity; +package org.pkwmtt.examCalendar.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Set; @@ -11,18 +12,15 @@ @Getter @Builder @AllArgsConstructor +@NoArgsConstructor @Table(name = "`general_group`") public class GeneralGroup { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer general_group_id; private String name; @OneToMany(mappedBy = "general_group") private Set groups; - - public GeneralGroup() { - - } } diff --git a/src/main/java/org/pkwmtt/entity/Group.java b/src/main/java/org/pkwmtt/examCalendar/entity/Group.java similarity index 72% rename from src/main/java/org/pkwmtt/entity/Group.java rename to src/main/java/org/pkwmtt/examCalendar/entity/Group.java index cba5eaf..050a5cc 100644 --- a/src/main/java/org/pkwmtt/entity/Group.java +++ b/src/main/java/org/pkwmtt/examCalendar/entity/Group.java @@ -1,18 +1,20 @@ -package org.pkwmtt.entity; +package org.pkwmtt.examCalendar.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Builder @AllArgsConstructor +@NoArgsConstructor @Table(name = "`groups`") public class Group { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer group_id; private String name; @@ -22,8 +24,4 @@ public class Group { @ManyToOne @JoinColumn(name = "general_group_id") private GeneralGroup general_group; - - public Group() { - - } } diff --git a/src/main/java/org/pkwmtt/entity/OTPCode.java b/src/main/java/org/pkwmtt/examCalendar/entity/OTPCode.java similarity index 75% rename from src/main/java/org/pkwmtt/entity/OTPCode.java rename to src/main/java/org/pkwmtt/examCalendar/entity/OTPCode.java index 76d8110..47d3a9a 100644 --- a/src/main/java/org/pkwmtt/entity/OTPCode.java +++ b/src/main/java/org/pkwmtt/examCalendar/entity/OTPCode.java @@ -1,9 +1,10 @@ -package org.pkwmtt.entity; +package org.pkwmtt.examCalendar.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; @@ -11,10 +12,11 @@ @Getter @Builder @AllArgsConstructor +@NoArgsConstructor @Table(name = "otp_codes") public class OTPCode { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer otp_code_id; private String code; @@ -26,8 +28,4 @@ public class OTPCode { @OneToOne @JoinColumn(name = "user_id", unique = true) private User user; - - public OTPCode() { - - } } diff --git a/src/main/java/org/pkwmtt/entity/User.java b/src/main/java/org/pkwmtt/examCalendar/entity/User.java similarity index 77% rename from src/main/java/org/pkwmtt/entity/User.java rename to src/main/java/org/pkwmtt/examCalendar/entity/User.java index 448f135..cb90f87 100644 --- a/src/main/java/org/pkwmtt/entity/User.java +++ b/src/main/java/org/pkwmtt/examCalendar/entity/User.java @@ -1,19 +1,21 @@ -package org.pkwmtt.entity; +package org.pkwmtt.examCalendar.entity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import org.pkwmtt.enums.Role; @Entity @Getter @Builder @AllArgsConstructor +@NoArgsConstructor @Table(name = "`users`") public class User { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer user_id; @ManyToOne @@ -28,8 +30,4 @@ public class User { @OneToOne(mappedBy = "user") private OTPCode otp_code; - - public User() { - - } } diff --git a/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapper.java b/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapper.java new file mode 100644 index 0000000..343f70e --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapper.java @@ -0,0 +1,49 @@ +package org.pkwmtt.examCalendar.mapper; + +import lombok.RequiredArgsConstructor; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.repository.ExamTypeRepository; +import org.pkwmtt.exceptions.ExamTypeNotExistsException; +import org.springframework.stereotype.Component; + +/** + * maps ExamDto to Exam entity. Couldn't be utility class, because needs ExamTypeRepository to validate exam types + */ +@Component +@RequiredArgsConstructor +public class ExamDtoToExamMapper { + private final ExamTypeRepository examTypeRepository; + + /** + * @param examDto examDto object received from request + * @return Exam entity WITHOUT examId which should be assigned by database + * Also contains examType field converted from String do ExamType + */ + public Exam mapToNewExam(ExamDto examDto) { + return Exam.builder() + .title(examDto.getTitle()) + .description(examDto.getDescription()) + .date(examDto.getDate()) + .examGroups(examDto.getExamGroups()) + .examType(examTypeRepository.findByName(examDto.getExamType()).orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType()))) + .build(); + } + + /** + * @param examDto examDto object received from request + * @param id of Exam that need to be modified + * @return Exam entity WITH examId that allow to update entity in database instead of creating new one + * Also contains examType field converted from String do ExamType + */ + public Exam mapToExistingExam(ExamDto examDto, int id) { + return Exam.builder() + .examId(id) + .title(examDto.getTitle()) + .description(examDto.getDescription()) + .date(examDto.getDate()) + .examGroups(examDto.getExamGroups()) + .examType(examTypeRepository.findByName(examDto.getExamType()).orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType()))) + .build(); + } +} diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java new file mode 100644 index 0000000..e6d3454 --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java @@ -0,0 +1,74 @@ +package org.pkwmtt.examCalendar.repository; + +import org.pkwmtt.examCalendar.entity.Exam; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Set; + +public interface ExamRepository extends JpaRepository { + + /** + * fetch all data using one query + * @param group1 group identifier + * @param group2 group identifier + * @param group3 group identifier + * @param group4 group identifier + * @return set of Exams for specific groups + */ + @Query("SELECT e FROM Exam e JOIN FETCH e.examType WHERE " + + "e.examGroups LIKE CONCAT('%', :g1, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g2, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g3, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g4, '%') ") + Set findExamsByGroupsIdentifier( + @Param("g1") String group1, + @Param("g2") String group2, + @Param("g3") String group3, + @Param("g4") String group4 + ); + + /** + * fetch all data using one query + * @param group1 group identifier + * @param group2 group identifier + * @param group3 group identifier + * @return set of Exams for specific groups + */ + @Query("SELECT e FROM Exam e JOIN FETCH e.examType WHERE " + + "e.examGroups LIKE CONCAT('%', :g1, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g2, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g3, '%') ") + Set findExamsByGroupsIdentifier( + @Param("g1") String group1, + @Param("g2") String group2, + @Param("g3") String group3 + ); + + /** + * fetch all data using one query + * @param group1 group identifier + * @param group2 group identifier + * @return set of Exams for specific groups + */ + @Query("SELECT e FROM Exam e JOIN FETCH e.examType WHERE " + + "e.examGroups LIKE CONCAT('%', :g1, '%') OR " + + "e.examGroups LIKE CONCAT('%', :g2, '%')" ) + Set findExamsByGroupsIdentifier( + @Param("g1") String group1, + @Param("g2") String group2 + ); + + /** + * fetch all data using one query + * @param group group identifier + * @return set of Exams for specific group + */ + @Query("SELECT e FROM Exam e JOIN FETCH e.examType WHERE " + + "e.examGroups LIKE CONCAT('%', :gg, '%')") + Set findExamsByGroupsIdentifier( + @Param("gg") String group + ); + +} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/ExamTypeRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/ExamTypeRepository.java new file mode 100644 index 0000000..c14d733 --- /dev/null +++ b/src/main/java/org/pkwmtt/examCalendar/repository/ExamTypeRepository.java @@ -0,0 +1,10 @@ +package org.pkwmtt.examCalendar.repository; + +import org.pkwmtt.examCalendar.entity.ExamType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ExamTypeRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/repository/GeneralGroupRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/GeneralGroupRepository.java similarity index 61% rename from src/main/java/org/pkwmtt/repository/GeneralGroupRepository.java rename to src/main/java/org/pkwmtt/examCalendar/repository/GeneralGroupRepository.java index a4c1c55..62f4fbb 100644 --- a/src/main/java/org/pkwmtt/repository/GeneralGroupRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/GeneralGroupRepository.java @@ -1,6 +1,6 @@ -package org.pkwmtt.repository; +package org.pkwmtt.examCalendar.repository; -import org.pkwmtt.entity.GeneralGroup; +import org.pkwmtt.examCalendar.entity.GeneralGroup; import org.springframework.data.jpa.repository.JpaRepository; public interface GeneralGroupRepository extends JpaRepository { diff --git a/src/main/java/org/pkwmtt/repository/GroupRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/GroupRepository.java similarity index 60% rename from src/main/java/org/pkwmtt/repository/GroupRepository.java rename to src/main/java/org/pkwmtt/examCalendar/repository/GroupRepository.java index ae46110..98bb7a3 100644 --- a/src/main/java/org/pkwmtt/repository/GroupRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/GroupRepository.java @@ -1,6 +1,6 @@ -package org.pkwmtt.repository; +package org.pkwmtt.examCalendar.repository; -import org.pkwmtt.entity.Group; +import org.pkwmtt.examCalendar.entity.Group; import org.springframework.data.jpa.repository.JpaRepository; public interface GroupRepository extends JpaRepository { diff --git a/src/main/java/org/pkwmtt/repository/OTPCodeRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/OTPCodeRepository.java similarity index 60% rename from src/main/java/org/pkwmtt/repository/OTPCodeRepository.java rename to src/main/java/org/pkwmtt/examCalendar/repository/OTPCodeRepository.java index 4f79485..848b4d4 100644 --- a/src/main/java/org/pkwmtt/repository/OTPCodeRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/OTPCodeRepository.java @@ -1,6 +1,6 @@ -package org.pkwmtt.repository; +package org.pkwmtt.examCalendar.repository; -import org.pkwmtt.entity.OTPCode; +import org.pkwmtt.examCalendar.entity.OTPCode; import org.springframework.data.jpa.repository.JpaRepository; public interface OTPCodeRepository extends JpaRepository { diff --git a/src/main/java/org/pkwmtt/repository/UserRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java similarity index 60% rename from src/main/java/org/pkwmtt/repository/UserRepository.java rename to src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java index 71ccd75..acdf767 100644 --- a/src/main/java/org/pkwmtt/repository/UserRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/UserRepository.java @@ -1,6 +1,6 @@ -package org.pkwmtt.repository; +package org.pkwmtt.examCalendar.repository; -import org.pkwmtt.entity.User; +import org.pkwmtt.examCalendar.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { diff --git a/src/main/java/org/pkwmtt/exceptions/ErrorResponseDTO.java b/src/main/java/org/pkwmtt/exceptions/ErrorResponseDTO.java deleted file mode 100644 index 0f44865..0000000 --- a/src/main/java/org/pkwmtt/exceptions/ErrorResponseDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.pkwmtt.exceptions; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@AllArgsConstructor -@Builder -@Getter -@NoArgsConstructor -public class ErrorResponseDTO { - private String message; - @Builder.Default - private LocalDateTime timestamp = LocalDateTime.now(); - - public ErrorResponseDTO(String message) { - this.message = message; - this.timestamp = LocalDateTime.now(); - } -} diff --git a/src/main/java/org/pkwmtt/exceptions/ExamTypeNotExistsException.java b/src/main/java/org/pkwmtt/exceptions/ExamTypeNotExistsException.java new file mode 100644 index 0000000..5e8171a --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/ExamTypeNotExistsException.java @@ -0,0 +1,7 @@ +package org.pkwmtt.exceptions; + +public class ExamTypeNotExistsException extends RuntimeException { + public ExamTypeNotExistsException(String examType) { + super("Invalid exam type " + examType); + } +} diff --git a/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java b/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java new file mode 100644 index 0000000..4faadac --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java @@ -0,0 +1,7 @@ +package org.pkwmtt.exceptions; + +public class InvalidGroupIdentifierException extends RuntimeException { + public InvalidGroupIdentifierException(String groupIdentifier) { + super("Invalid group identifier: " + groupIdentifier); + } +} diff --git a/src/main/java/org/pkwmtt/exceptions/MailServiceNotAvailableException.java b/src/main/java/org/pkwmtt/exceptions/MailServiceNotAvailableException.java new file mode 100644 index 0000000..e5f98de --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/MailServiceNotAvailableException.java @@ -0,0 +1,9 @@ +package org.pkwmtt.exceptions; + +public class MailServiceNotAvailableException + extends RuntimeException { + public MailServiceNotAvailableException () { + super("Mail service is not available right now."); + } + +} diff --git a/src/main/java/org/pkwmtt/exceptions/NoSuchElementWithProvidedIdException.java b/src/main/java/org/pkwmtt/exceptions/NoSuchElementWithProvidedIdException.java new file mode 100644 index 0000000..e17eead --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/NoSuchElementWithProvidedIdException.java @@ -0,0 +1,7 @@ +package org.pkwmtt.exceptions; + +public class NoSuchElementWithProvidedIdException extends RuntimeException{ + public NoSuchElementWithProvidedIdException(int id) { + super("No such element with id: " + id); + } +} diff --git a/src/main/java/org/pkwmtt/exceptions/SpecifiedGeneralGroupDoesntExistsException.java b/src/main/java/org/pkwmtt/exceptions/SpecifiedGeneralGroupDoesntExistsException.java index 7ab02ab..f163b42 100644 --- a/src/main/java/org/pkwmtt/exceptions/SpecifiedGeneralGroupDoesntExistsException.java +++ b/src/main/java/org/pkwmtt/exceptions/SpecifiedGeneralGroupDoesntExistsException.java @@ -1,9 +1,11 @@ package org.pkwmtt.exceptions; -import net.bytebuddy.asm.Advice; - public class SpecifiedGeneralGroupDoesntExistsException extends RuntimeException { public SpecifiedGeneralGroupDoesntExistsException() { super("Specified general group doesn't exists"); } + + public SpecifiedGeneralGroupDoesntExistsException(String generalGroupName) { + super(String.format("Specified general group [%s] doesn't exists", generalGroupName)); + } } diff --git a/src/main/java/org/pkwmtt/exceptions/SpecifiedSubGroupDoesntExistsException.java b/src/main/java/org/pkwmtt/exceptions/SpecifiedSubGroupDoesntExistsException.java new file mode 100644 index 0000000..3fe095a --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/SpecifiedSubGroupDoesntExistsException.java @@ -0,0 +1,12 @@ +package org.pkwmtt.exceptions; + +public class SpecifiedSubGroupDoesntExistsException + extends RuntimeException { + public SpecifiedSubGroupDoesntExistsException () { + super("Specified sub group doesn't exists"); + } + + public SpecifiedSubGroupDoesntExistsException (String subgroupName) { + super(String.format("Specified sub group [%s] doesn't exists", subgroupName)); + } +} diff --git a/src/main/java/org/pkwmtt/exceptions/UnsupportedCountOfArgumentsException.java b/src/main/java/org/pkwmtt/exceptions/UnsupportedCountOfArgumentsException.java new file mode 100644 index 0000000..709978a --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/UnsupportedCountOfArgumentsException.java @@ -0,0 +1,8 @@ +package org.pkwmtt.exceptions; + +public class UnsupportedCountOfArgumentsException extends RuntimeException { + public UnsupportedCountOfArgumentsException(int expectedMin, int expectedMax, int provided) { + super("Invalid count of arguments provided: " + provided + + " expected more than: " + expectedMin + " less than: " + expectedMax); + } +} diff --git a/src/main/java/org/pkwmtt/exceptions/WebPageContentNotAvailableException.java b/src/main/java/org/pkwmtt/exceptions/WebPageContentNotAvailableException.java index 5733e6c..632308a 100644 --- a/src/main/java/org/pkwmtt/exceptions/WebPageContentNotAvailableException.java +++ b/src/main/java/org/pkwmtt/exceptions/WebPageContentNotAvailableException.java @@ -1,10 +1,6 @@ package org.pkwmtt.exceptions; public class WebPageContentNotAvailableException extends RuntimeException { - public WebPageContentNotAvailableException(String message) { - super(message); - } - public WebPageContentNotAvailableException() { super("Content of university webpage is not available right now."); } diff --git a/src/main/java/org/pkwmtt/exceptions/dto/ErrorResponseDTO.java b/src/main/java/org/pkwmtt/exceptions/dto/ErrorResponseDTO.java new file mode 100644 index 0000000..568e9bc --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/dto/ErrorResponseDTO.java @@ -0,0 +1,16 @@ +package org.pkwmtt.exceptions.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +@Data +public class ErrorResponseDTO { + private String message; + private LocalDateTime timestamp; + + public ErrorResponseDTO(String message) { + this.message = message; + this.timestamp = LocalDateTime.now(); + } +} diff --git a/src/main/java/org/pkwmtt/mail/EmailService.java b/src/main/java/org/pkwmtt/mail/EmailService.java new file mode 100644 index 0000000..1174ec0 --- /dev/null +++ b/src/main/java/org/pkwmtt/mail/EmailService.java @@ -0,0 +1,46 @@ +package org.pkwmtt.mail; + +import jakarta.annotation.PostConstruct; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.pkwmtt.exceptions.MailServiceNotAvailableException; +import org.pkwmtt.mail.config.MailConfig; +import org.pkwmtt.mail.dto.MailDTO; +import org.springframework.core.env.Environment; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class EmailService { + private final Environment environment; + + private final JavaMailSender mailSender; + + private String hostEmail; + + @PostConstruct + private void assignProperties () { + hostEmail = environment.getProperty("spring.mail.username"); + } + + public void send (MailDTO mail) throws MessagingException, MailServiceNotAvailableException { + if (!MailConfig.isEnabled()) { + throw new MailServiceNotAvailableException(); + } + + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(hostEmail); + helper.setTo(mail.getRecipient()); + helper.setText(mail.getDescription(), true); + helper.setSubject(mail.getTitle()); + + mailSender.send(message); + + } +} diff --git a/src/main/java/org/pkwmtt/mail/EmailTempController.java b/src/main/java/org/pkwmtt/mail/EmailTempController.java new file mode 100644 index 0000000..1048a19 --- /dev/null +++ b/src/main/java/org/pkwmtt/mail/EmailTempController.java @@ -0,0 +1,35 @@ +package org.pkwmtt.mail; + +import jakarta.mail.MessagingException; +import lombok.RequiredArgsConstructor; +import org.pkwmtt.exceptions.MailServiceNotAvailableException; +import org.pkwmtt.exceptions.dto.ErrorResponseDTO; +import org.pkwmtt.mail.dto.MailDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/mail") +public class EmailTempController { + + private final EmailService service; + + @PostMapping + public void sendMail (@RequestParam(name = "r") String recipientEmailAddress) + throws MessagingException, MailServiceNotAvailableException { + service.send(new MailDTO() + .setRecipient(recipientEmailAddress) + .setDescription("TEST") + .setTitle("TEST")); + } + + @ExceptionHandler(MailServiceNotAvailableException.class) + public ResponseEntity handle (Exception e) { + return new ResponseEntity<>( + new ErrorResponseDTO(e.getMessage()), + HttpStatus.SERVICE_UNAVAILABLE + ); + } +} diff --git a/src/main/java/org/pkwmtt/mail/config/MailConfig.java b/src/main/java/org/pkwmtt/mail/config/MailConfig.java new file mode 100644 index 0000000..595d744 --- /dev/null +++ b/src/main/java/org/pkwmtt/mail/config/MailConfig.java @@ -0,0 +1,57 @@ +package org.pkwmtt.mail.config; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +@RequiredArgsConstructor +public class MailConfig { + + @Getter + private static boolean enabled = true; + + private final Environment environment; + + private String username; + private String password; + + @PostConstruct + private void assignAndValidateProperties () { + username = environment.getProperty("spring.mail.username"); + password = environment.getProperty("spring.mail.password"); + + if (username == null || password == null || username.isEmpty() || password.isEmpty()) { + enabled = false; + } + } + + @Bean + public JavaMailSender javaMailSender () { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + + if (!enabled) { + return mailSender; + } + + mailSender.setHost("smtp.gmail.com"); + mailSender.setPort(587); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + + return mailSender; + } + +} diff --git a/src/main/java/org/pkwmtt/mail/dto/MailDTO.java b/src/main/java/org/pkwmtt/mail/dto/MailDTO.java new file mode 100644 index 0000000..215d774 --- /dev/null +++ b/src/main/java/org/pkwmtt/mail/dto/MailDTO.java @@ -0,0 +1,14 @@ +package org.pkwmtt.mail.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class MailDTO { + private String recipient; + private String title; + private String description; +} diff --git a/src/main/java/org/pkwmtt/repository/ExamRepository.java b/src/main/java/org/pkwmtt/repository/ExamRepository.java deleted file mode 100644 index 2faafaa..0000000 --- a/src/main/java/org/pkwmtt/repository/ExamRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.pkwmtt.repository; - -import org.pkwmtt.entity.Exam; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ExamRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/repository/ExamTypeRepository.java b/src/main/java/org/pkwmtt/repository/ExamTypeRepository.java deleted file mode 100644 index 1b7d38c..0000000 --- a/src/main/java/org/pkwmtt/repository/ExamTypeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.pkwmtt.repository; - -import org.pkwmtt.entity.ExamType; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ExamTypeRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java b/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java new file mode 100644 index 0000000..6af41a8 --- /dev/null +++ b/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java @@ -0,0 +1,26 @@ +package org.pkwmtt.status; + + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.sql.SQLException; + +@Slf4j +@Service +public class DatabaseStatusChecker { + @Getter + private static boolean enabled = false; + + @Autowired + DatabaseStatusChecker (DataSource dataSource) { + try { + enabled = dataSource.getConnection().isValid(2); + } catch (SQLException e) { + log.error("Couldn't check database connection. Service will be unavailable"); + } + } +} diff --git a/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java b/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java new file mode 100644 index 0000000..e7a4731 --- /dev/null +++ b/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java @@ -0,0 +1,47 @@ +package org.pkwmtt.status; + +import jakarta.annotation.PostConstruct; +import org.pkwmtt.mail.config.MailConfig; +import org.pkwmtt.timetable.TimetableCacheService; +import org.pkwmtt.timetable.TimetableService; +import org.springframework.stereotype.Service; + + +@Service +public class SystemStatusCheckerService { + + private String mailingStatus; + private String databaseStatus; + private String cacheStatus; + private String timetableStatus; + + SystemStatusCheckerService () { + checkStatuses(); + } + + @PostConstruct + private void checkStatuses () { + mailingStatus = assignStatus(MailConfig.isEnabled()); + databaseStatus = assignStatus(DatabaseStatusChecker.isEnabled()); + timetableStatus = assignStatus(TimetableService.isEnabled()); + cacheStatus = assignStatus(TimetableCacheService.isCacheAvailable()); + } + + public String getStatus () { + return String.format( + """ + Server: ✅; + Services: + Mail: %s + Database: %s, + Timetable: %s, + Cache: %s + """, mailingStatus, databaseStatus, timetableStatus, cacheStatus + ); + } + + + private String assignStatus (boolean condition) { + return condition ? "✅" : "❌"; + } +} diff --git a/src/main/java/org/pkwmtt/status/SystemStatusController.java b/src/main/java/org/pkwmtt/status/SystemStatusController.java new file mode 100644 index 0000000..dd055c1 --- /dev/null +++ b/src/main/java/org/pkwmtt/status/SystemStatusController.java @@ -0,0 +1,20 @@ +package org.pkwmtt.status; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/pkwmtt/system/status") +@RequiredArgsConstructor +public class SystemStatusController { + private final SystemStatusCheckerService service; + + @GetMapping + public ResponseEntity getSystemStatus () { + return ResponseEntity.ok(service.getStatus()); + } + +} diff --git a/src/main/java/org/pkwmtt/timetable/CacheableTimetableService.java b/src/main/java/org/pkwmtt/timetable/CacheableTimetableService.java deleted file mode 100644 index be76b87..0000000 --- a/src/main/java/org/pkwmtt/timetable/CacheableTimetableService.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.pkwmtt.timetable; - -import lombok.RequiredArgsConstructor; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; -import org.pkwmtt.exceptions.WebPageContentNotAvailableException; -import org.pkwmtt.timetable.dto.TimetableDTO; -import org.pkwmtt.timetable.parser.TimetableParserService; -import org.springframework.cache.annotation.CacheConfig; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -@Service -@RequiredArgsConstructor -@CacheConfig(cacheNames = "timetables") -public class CacheableTimetableService { - private final TimetableParserService parser; - - /** - * Fetches and parses the full timetable for a general group. - * - * @param generalGroupName group to fetch - * @return parsed timetable - * @throws WebPageContentNotAvailableException if remote content is unavailable - */ - @Cacheable(key = "#generalGroupName") - public TimetableDTO getGeneralGroupSchedule(String generalGroupName) throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException { - var generalGroupList = getGeneralGroupsList(); - - if (!generalGroupList.containsKey(generalGroupName)){ - throw new SpecifiedGeneralGroupDoesntExistsException(); - } - - Document document; - String url = generalGroupList.get(generalGroupName); - try { - document = Jsoup - .connect(String.format("https://podzial.mech.pk.edu.pl/stacjonarne/html/%s", url)) - .get(); - } catch (IOException ioe) { - throw new WebPageContentNotAvailableException(); - } - - return new TimetableDTO(generalGroupName, parser.parse(document.html())); - } - - /** - * Retrieves a mapping of general group names to their corresponding timetable URLs. - * - * @return map of group names to URLs - * @throws WebPageContentNotAvailableException if the source page can't be fetched - */ - @Cacheable(key = "'generalGroupList'") - public Map getGeneralGroupsList() throws WebPageContentNotAvailableException { - Document document; - try { - document = Jsoup - .connect("http://podzial.mech.pk.edu.pl/stacjonarne/html/lista.html") - .get(); - } catch (IOException ioe) { - throw new WebPageContentNotAvailableException(); - } - - return parser.parseGeneralGroups(document.html()); - } - - - /** - * Retrieves the standard list of hour ranges used in the timetable. - * - * @return list of hour labels (e.g., 08:00–09:30) - * @throws WebPageContentNotAvailableException if hour definition page can't be loaded - */ - @Cacheable(key = "'hoursList'") - public List getListOfHours() throws WebPageContentNotAvailableException { - try { - Document document = Jsoup - .connect("https://podzial.mech.pk.edu.pl/stacjonarne/html/plany/o25.html") - .get(); - - return parser.parseHours(document.html()); - } catch (IOException ioe) { - throw new WebPageContentNotAvailableException(); - } - } -} diff --git a/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java b/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java new file mode 100644 index 0000000..6bd4861 --- /dev/null +++ b/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java @@ -0,0 +1,166 @@ +package org.pkwmtt.timetable; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.jsoup.Jsoup; +import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; +import org.pkwmtt.exceptions.WebPageContentNotAvailableException; +import org.pkwmtt.timetable.dto.TimetableDTO; +import org.pkwmtt.timetable.parser.TimetableParserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.isNull; + +@Service +public class TimetableCacheService { + private final TimetableParserService parser; + private final ObjectMapper mapper; + private final Cache cache; + + @Getter + private static boolean cacheAvailable = true; + + @Value("${main.url:https://podzial.mech.pk.edu.pl/stacjonarne/html/}") + private String mainUrl; + + public TimetableCacheService (TimetableParserService parser, ObjectMapper mapper, CacheManager cacheManager) { + this.parser = parser; + this.mapper = mapper; + cache = cacheManager.getCache("timetables"); + + if (isNull(cache)) { + cacheAvailable = false; + } + } + + /** + * @return connection status + */ + public static boolean isConnectionAvailable () { + try { + fetchData("https://podzial.mech.pk.edu.pl/stacjonarne/html/"); + return true; + } catch (Exception e) { + System.out.println(e.getMessage()); + return false; + } + } + + /** + * Fetches and parses the full timetable for a general group. + * + * @param generalGroupName group to fetch + * @return parsed timetable + * @throws WebPageContentNotAvailableException if remote content is unavailable + */ + public TimetableDTO getGeneralGroupSchedule (String generalGroupName) + throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException { + var generalGroupList = getGeneralGroupsMap(); + + if (!generalGroupList.containsKey(generalGroupName)) { + throw new SpecifiedGeneralGroupDoesntExistsException(generalGroupName); + } + + String groupUrl = generalGroupList.get(generalGroupName); + String url = mainUrl + groupUrl; + String cacheKey = "timetable_" + generalGroupName; + var html = fetchData(url); + String json = cache.get( + cacheKey, () -> { + var timetableDTO = new TimetableDTO(generalGroupName, parser.parse(html)); + return mapper.writeValueAsString(timetableDTO); + } + ); + + return getMappedValue( + json, cacheKey, cache, new TypeReference<>() { + } + ); + } + + /** + * Retrieves a mapping of general group names to their corresponding timetable URLs. + * + * @return map of group names to URLs + * @throws WebPageContentNotAvailableException if the source page can't be fetched + */ + public Map getGeneralGroupsMap () throws WebPageContentNotAvailableException { + var url = mainUrl + "lista.html"; + var html = fetchData(url); + String json = cache.get( + "generalGroupMap", + () -> mapper.writeValueAsString(parser.parseGeneralGroups(html)) + ); + + return getMappedValue( + json, "generalGroupList", cache, new TypeReference<>() { + } + ); + } + + /** + * Retrieves the standard list of hour ranges used in the timetable. + * + * @return list of hour labels (e.g., 08:00–09:30) + * @throws WebPageContentNotAvailableException if hour definition page can't be loaded + */ + public List getListOfHours () throws WebPageContentNotAvailableException { + String url = mainUrl + "plany/o25.html"; + String json = cache.get( + "hourList", + () -> mapper.writeValueAsString(parser.parseHours(fetchData(url))) + ); + + List result = getMappedValue( + json, "hourList", cache, new TypeReference<>() { + } + ); + + //Delete useless spaces + result = result.stream().map(item -> item.replaceAll(" ", "")).toList(); + + return result; + } + + /** + * @param json - json representation of java object + * @param key - cache key + * @param cache - cache object + * @param targetClass - type to map value to + * @param type of object + * @return java object type + * @throws WebPageContentNotAvailableException if there were trouble with fetching data + */ + private T getMappedValue (String json, String key, Cache cache, TypeReference targetClass) + throws WebPageContentNotAvailableException { + try { + return mapper.readValue(json, targetClass); + } catch (JsonProcessingException e) { + cache.evict(key); + throw new WebPageContentNotAvailableException(); + } + } + + /** + * @param url - url of webpage + * @return html code of selected webpage + * @throws WebPageContentNotAvailableException if there were trouble with fetching data + */ + private static String fetchData (String url) throws WebPageContentNotAvailableException { + try { + return Jsoup.connect(url).get().html(); + } catch (IOException ioe) { + throw new WebPageContentNotAvailableException(); + } + } + +} diff --git a/src/main/java/org/pkwmtt/timetable/TimetableController.java b/src/main/java/org/pkwmtt/timetable/TimetableController.java index 613ad37..7110810 100644 --- a/src/main/java/org/pkwmtt/timetable/TimetableController.java +++ b/src/main/java/org/pkwmtt/timetable/TimetableController.java @@ -2,41 +2,45 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; -import org.pkwmtt.exceptions.ErrorResponseDTO; import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; +import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; import org.pkwmtt.exceptions.WebPageContentNotAvailableException; import org.pkwmtt.timetable.dto.TimetableDTO; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.Collections; import java.util.List; +import static java.util.Objects.isNull; + @RestController @RequestMapping("/pkmwtt/api/v1/timetables") @RequiredArgsConstructor public class TimetableController { private final TimetableService service; - private final CacheableTimetableService cacheableService; + private final TimetableCacheService cachedService; /** * Provide schedule of specified group and filters if all provided * * @param generalGroupName name of general group - * @param sub list of subgroups + * @param subgroups list of subgroups * @return schedule of specified group with provided filters * @throws WebPageContentNotAvailableException . */ @GetMapping("/{generalGroupName}") - public ResponseEntity getGeneralGroupSchedule(@PathVariable String generalGroupName, @RequestParam(required = false) List sub) - throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException { - if (sub == null || sub.isEmpty()) - return ResponseEntity.ok(cacheableService.getGeneralGroupSchedule(generalGroupName)); - - sub = sub.stream().map(String::toUpperCase).toList(); + public ResponseEntity getGeneralGroupSchedule ( + @PathVariable String generalGroupName, + @RequestParam(required = false, name = "sub") List subgroups) + throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, + SpecifiedSubGroupDoesntExistsException, JsonProcessingException { - return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule(generalGroupName.toUpperCase(), sub)); + if (isNull(subgroups) || subgroups.isEmpty()) { + return ResponseEntity.ok(cachedService.getGeneralGroupSchedule(generalGroupName)); + } + return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule( + generalGroupName, subgroups + )); } /** @@ -46,8 +50,9 @@ public ResponseEntity getGeneralGroupSchedule(@PathVariable String * @throws WebPageContentNotAvailableException . */ @GetMapping("/hours") - public ResponseEntity> getListOfHours() throws WebPageContentNotAvailableException { - return ResponseEntity.ok(cacheableService.getListOfHours()); + public ResponseEntity> getListOfHours () + throws WebPageContentNotAvailableException { + return ResponseEntity.ok(cachedService.getListOfHours()); } /** @@ -56,10 +61,9 @@ public ResponseEntity> getListOfHours() throws WebPageContentNotAva * @return list of general groups */ @GetMapping("/groups/general") - public ResponseEntity> getListOfGeneralGroups() { - var result = new java.util.ArrayList<>(cacheableService.getGeneralGroupsList().keySet().stream().toList()); - Collections.sort(result); - return ResponseEntity.ok(result); + public ResponseEntity> getListOfGeneralGroups () + throws WebPageContentNotAvailableException { + return ResponseEntity.ok(service.getGeneralGroupList()); } /** @@ -70,27 +74,11 @@ public ResponseEntity> getListOfGeneralGroups() { * @throws JsonProcessingException . */ @GetMapping("/groups/{generalGroupName}") - public ResponseEntity> getListOfAvailableGroups(@PathVariable String generalGroupName) - throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, WebPageContentNotAvailableException { - return ResponseEntity.ok(service.getAvailableSubGroups(generalGroupName.toUpperCase())); - } - - @ExceptionHandler(WebPageContentNotAvailableException.class) - @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) - public ResponseEntity handleWebPageContentNotAvailableException(WebPageContentNotAvailableException e) { - return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.SERVICE_UNAVAILABLE); + public ResponseEntity> getListOfAvailableGroups (@PathVariable String generalGroupName) + throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, + WebPageContentNotAvailableException { + return ResponseEntity.ok(service.getAvailableSubGroups(generalGroupName)); } - @ExceptionHandler(JsonProcessingException.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ResponseEntity handleJsonProcessingException() { - return new ResponseEntity<>(new ErrorResponseDTO(""), HttpStatus.INTERNAL_SERVER_ERROR); - } - - @ExceptionHandler(SpecifiedGeneralGroupDoesntExistsException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ResponseEntity handleSpecifiedGeneralGroupDoesntExistsException(SpecifiedGeneralGroupDoesntExistsException e) { - return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST); - } } diff --git a/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java b/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java new file mode 100644 index 0000000..bb92055 --- /dev/null +++ b/src/main/java/org/pkwmtt/timetable/TimetableExceptionHandler.java @@ -0,0 +1,54 @@ +package org.pkwmtt.timetable; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.slf4j.Slf4j; +import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; +import org.pkwmtt.exceptions.dto.ErrorResponseDTO; +import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; +import org.pkwmtt.exceptions.WebPageContentNotAvailableException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@SuppressWarnings({"LoggingSimilarMessage", "StringConcatenationArgumentToLogCall"}) +@Slf4j +@RestControllerAdvice(assignableTypes = {TimetableController.class}) +public class TimetableExceptionHandler { + @ExceptionHandler(WebPageContentNotAvailableException.class) + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + public ResponseEntity handleWebPageContentNotAvailableException (WebPageContentNotAvailableException e) { + log.error("SERVICE_UNAVAILABLE # " + e.getMessage()); + return new ResponseEntity<>( + new ErrorResponseDTO(e.getMessage()), + HttpStatus.SERVICE_UNAVAILABLE + ); + } + + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity handleJsonProcessingException (JsonProcessingException e) { + log.error("INTERNAL_SERVER_ERROR # " + e.getMessage()); + return new ResponseEntity<>( + new ErrorResponseDTO("Json Processing Failed"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + @ExceptionHandler({SpecifiedGeneralGroupDoesntExistsException.class, SpecifiedSubGroupDoesntExistsException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleSpecifiedGeneralGroupDoesntExistsException (Exception e) { + return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IllegalAccessException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity handleIllegalAccessException (IllegalAccessException e) { + log.error("INTERNAL_SERVER_ERROR # " + e.getMessage()); + return new ResponseEntity<>( + new ErrorResponseDTO(e.getMessage()), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } +} diff --git a/src/main/java/org/pkwmtt/timetable/TimetableService.java b/src/main/java/org/pkwmtt/timetable/TimetableService.java index 15ab483..849cd66 100644 --- a/src/main/java/org/pkwmtt/timetable/TimetableService.java +++ b/src/main/java/org/pkwmtt/timetable/TimetableService.java @@ -2,24 +2,36 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; +import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; import org.pkwmtt.exceptions.WebPageContentNotAvailableException; import org.pkwmtt.timetable.dto.DayOfWeekDTO; import org.pkwmtt.timetable.dto.TimetableDTO; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; @Slf4j @Service -@RequiredArgsConstructor public class TimetableService { - private final CacheableTimetableService cacheableTimetableService; - + private final TimetableCacheService cachedService; + + @Getter + private static final boolean enabled = TimetableCacheService.isConnectionAvailable(); + + @Autowired + TimetableService (TimetableCacheService cachedService) { + this.cachedService = cachedService; + } + /** * Parses the timetable JSON to extract subgroup identifiers like K01, P03, GL04 using regex. * @@ -27,36 +39,38 @@ public class TimetableService { * @return sorted list of subgroup names found in the timetable * @throws JsonProcessingException if timetable conversion to JSON fails */ - public List getAvailableSubGroups(String generalGroupName) - throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, WebPageContentNotAvailableException { + public List getAvailableSubGroups (String generalGroupName) + throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, + WebPageContentNotAvailableException { + + generalGroupName = generalGroupName.toUpperCase(); + TimetableDTO timetable = cachedService.getGeneralGroupSchedule(generalGroupName); + ObjectMapper mapper = new ObjectMapper(); - TimetableDTO timetable = cacheableTimetableService.getGeneralGroupSchedule(generalGroupName); String timeTableAsJson = mapper.writeValueAsString(timetable); - + // Regex pattern for group codes like K01, GP03, L04, etc. String regex = "\\bG?[KPL]0[0-9]\\b"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(timeTableAsJson); - + Set matchedGroups = new HashSet<>(); - + //Check if text starts with 'G' and delete it // to match frontend requirements String text; while (matcher.find()) { text = matcher.group(); - if (text.startsWith("G")) + if (text.startsWith("G")) { text = text.substring(1); + } matchedGroups.add(text); } - - List result = new ArrayList<>(matchedGroups.stream().toList()); - Collections.sort(result); - - return result; + + return matchedGroups.stream().sorted().toList(); } - - + + /** * Retrieves timetable and filters entries based on subgroups parameters * @@ -65,16 +79,44 @@ public List getAvailableSubGroups(String generalGroupName) * @return filtered timetable * @throws WebPageContentNotAvailableException if source data can't be retrieved */ - public TimetableDTO getFilteredGeneralGroupSchedule(String generalGroupName, List sub) throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException { - List schedule = cacheableTimetableService.getGeneralGroupSchedule(generalGroupName).getData(); - - for (var day : schedule) + public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, List sub) + throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, + JsonProcessingException { + + generalGroupName = generalGroupName.toUpperCase(); + + //Check if specified subgroup is available for this generalGroup + var subgroups = getAvailableSubGroups(generalGroupName); + for (var group : sub) { + if (!subgroups.contains(group)) { + throw new SpecifiedSubGroupDoesntExistsException(group); + } + } + + List schedule = cachedService + .getGeneralGroupSchedule(generalGroupName) + .getData(); + + + for (var day : schedule) { sub.forEach(day::filterByGroup); - + } + schedule.forEach(DayOfWeekDTO::deleteSubjectTypesFromNames); - + return new TimetableDTO(generalGroupName, schedule); } - - + + /** + * @return List of general group's names + */ + public List getGeneralGroupList () throws WebPageContentNotAvailableException { + return cachedService + .getGeneralGroupsMap() + .keySet() + .stream() + .sorted() + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java b/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java index 50d6403..53d581b 100644 --- a/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java +++ b/src/main/java/org/pkwmtt/timetable/dto/DayOfWeekDTO.java @@ -1,41 +1,39 @@ package org.pkwmtt.timetable.dto; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; import java.util.regex.Pattern; -@Setter -@Getter + +@Data public class DayOfWeekDTO { private final String name; private List odd; private List even; - - public DayOfWeekDTO(String name) { + + public DayOfWeekDTO (String name) { this.name = name; odd = new ArrayList<>(); even = new ArrayList<>(); } - - - public void add(SubjectDTO subjectDTO, boolean isNotOdd) { + + + public void add (SubjectDTO subjectDTO, boolean isNotOdd) { if (isNotOdd) { even.add(subjectDTO); } else { odd.add(subjectDTO); } } - - - public void deleteSubjectTypesFromNames() { - even.forEach(SubjectDTO::deleteTypeFromName); - odd.forEach(SubjectDTO::deleteTypeFromName); + + + public void deleteSubjectTypesFromNames () { + even.forEach(SubjectDTO::deleteTypeAndUnnecessaryCharactersFromName); + odd.forEach(SubjectDTO::deleteTypeAndUnnecessaryCharactersFromName); } - + /** * Filters both odd- and even-week subject lists, * keeping only those entries that belong exclusively @@ -45,23 +43,24 @@ public void deleteSubjectTypesFromNames() { * where the first character is the group letter * and the last character is the subgroup number */ - public void filterByGroup(String group) { + public void filterByGroup (String group) { // Delete first character if group starts 'G' - if (group.charAt(0) == 'G' && group.length() > 3) + if (group.charAt(0) == 'G' && group.length() > 3) { group = group.substring(1); - + } + // Extract the group letter (e.g., "K" from "K03") - String groupName = Character.toString(group.charAt(0)); - + var groupName = String.valueOf(group.charAt(0)); + // Extract the subgroup digit (e.g., "3" from "K03") - String targetNumber = Character.toString(group.charAt(group.length() - 1)); - + var targetNumber = String.valueOf(group.charAt(group.length() - 1)); + // Apply the filter to both odd- and even-week lists odd = filter(odd, groupName, targetNumber); even = filter(even, groupName, targetNumber); - + } - + /** * Returns a new list containing only those SubjectDTO items * whose type string matches exclusively the target group code or doesn't have group at all. @@ -71,22 +70,16 @@ public void filterByGroup(String group) { * @param targetNumber the subgroup digit to keep (e.g., "3") * @return a filtered list of SubjectDTO */ - private List filter(List list, String groupName, String targetNumber) { - + private List filter (List list, String groupName, String targetNumber) { + list = list.stream() - // Keep only items that have no other subgroup codes - .filter( - item -> - hasOnlyTargetGroup( - item.getName(), - groupName, - targetNumber - ) - ).toList(); - + // Keep only items that have no other subgroup codes + .filter(item -> hasOnlyTargetGroup(item.getName(), groupName, targetNumber)) + .toList(); + return list; } - + /** * Checks if the given element string contains no other codes for the same group.* * @@ -95,15 +88,23 @@ private List filter(List list, String groupName, String * @param targetNumber the digit we want to allow (e.g., "3") * @return true if no non-target subgroup codes are present */ - private boolean hasOnlyTargetGroup(String element, String groupName, String targetNumber) { - Pattern pattern = Pattern.compile(String.format("\\b[%s]0[1-9]\\b", groupName)); - Matcher matcher = pattern.matcher(element); - if (!matcher.find()) + private boolean hasOnlyTargetGroup (String element, String groupName, String targetNumber) { + var pattern = Pattern.compile(String.format( + "\\bG?[%s]0[1-9]\\b", + Pattern.quote(groupName) + )); + var matcher = pattern.matcher(element); + if (!matcher.find()) { return true; - - pattern = Pattern.compile(String.format("%s0%s", groupName, targetNumber)); + } + + pattern = Pattern.compile(String.format( + "%s0%s", + Pattern.quote(groupName), + Pattern.quote(targetNumber) + )); matcher = pattern.matcher(element); return matcher.find(); } - + } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java index d3a9f55..7b71ce3 100644 --- a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java +++ b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java @@ -1,12 +1,13 @@ package org.pkwmtt.timetable.dto; import lombok.*; -import org.pkwmtt.timetable.enums.SubjectType; +import lombok.experimental.Accessors; +import org.pkwmtt.enums.SubjectType; -@Builder -@Getter -@Setter -@AllArgsConstructor +import java.util.regex.Pattern; + +@Data +@Accessors(chain = true) public class SubjectDTO { private String name; private String classroom; @@ -14,8 +15,13 @@ public class SubjectDTO { private SubjectType type; - public void deleteTypeFromName() { + public void deleteTypeAndUnnecessaryCharactersFromName() { if (name.contains(" ")) - this.name = name.substring(0,name.indexOf(' ')); + this.name = name.substring(0, name.indexOf(' ')); + + name = name + .replaceAll("_", " ") + .replaceAll(Pattern.quote("("), "") + .replaceAll(Pattern.quote(")"), ""); } } diff --git a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java index 085b8b8..af32752 100644 --- a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java +++ b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java @@ -7,7 +7,7 @@ import org.jsoup.select.Elements; import org.pkwmtt.timetable.dto.DayOfWeekDTO; import org.pkwmtt.timetable.dto.SubjectDTO; -import org.pkwmtt.timetable.enums.SubjectType; +import org.pkwmtt.enums.SubjectType; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -132,12 +132,11 @@ private SubjectDTO buildSubject(String rawName, String rawClassroom, int rowId) String classroom = cleanClassroomName(rawClassroom); SubjectType type = extractSubjectTypeFromName(name); - return SubjectDTO.builder() - .name(name) - .classroom(classroom) - .rowId(rowId) - .type(type) - .build(); + return new SubjectDTO() + .setName(name) + .setClassroom(classroom) + .setRowId(rowId) + .setType(type); } /** diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..cd609c7 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,17 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/pktt?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +spring.datasource.username=pkttuser +spring.datasource.password=pkttpassword + +server.port=8080 +server.address=0.0.0.0 + +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false +spring.jpa.hibernate.ddl-auto=none +spring.datasource.hikari.initialization-fail-timeout=0 + +logging.file.name=logs/app.log +logging.file.path=logs + +spring.cache.type=caffeine diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f9244d0..a227ab6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,6 @@ +#Import .env variables +spring.config.import=optional:file:.env[.properties] + spring.datasource.url=jdbc:mysql://localhost:3306/pktt?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC spring.datasource.username=pkttuser spring.datasource.password=pkttpassword @@ -10,3 +13,17 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false spring.jpa.hibernate.ddl-auto=none spring.datasource.hikari.initialization-fail-timeout=0 + +logging.file.name=logs/app.log +logging.file.path=logs + +spring.cache.type=caffeine + +logging.level.WireMock.my-mock=off + +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${EMAIL_USERNAME:} +spring.mail.password=${EMAIL_PASSWORD:} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..dcbbeca --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + true + + + %d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n + + + + INFO + + + + + + logs/app.log + true + + %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n + + + ERROR + + + + + + + + + + diff --git a/src/test/java/org/pkwmtt/ValuesForTest.java b/src/test/java/org/pkwmtt/ValuesForTest.java new file mode 100644 index 0000000..dd5e92b --- /dev/null +++ b/src/test/java/org/pkwmtt/ValuesForTest.java @@ -0,0 +1,269 @@ +package org.pkwmtt; + +public interface ValuesForTest { + + String timetableHTML = """ + + + + + + Plan lekcji oddziału - 12K1 + + + + + + + + + + +
12K1
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NrGodzPoniedziałekWtorekŚrodaCzwartekPiątek
17:30- 8:15PInterfUż W-(N) #PIU J207.1-n
Proj3D W-(P) #3-D J207.1-p
    
28:15- 9:00PInterfUż W-(N) #PIU J207.1-n
Proj3D W-(P) #3-D J207.1-p
  GodzPrzem-(P) #GdP C04-p 
39:15-10:00Mechatro P04-(n. #Mtr K227-n
Proj3D K04-(p. #3D J207.1-p
Proj3D K01-(n. Do J207.1-n
PSieciKP L02-(N. AP G110-n
PSieciKP L02-(P. PA G110-p
Mechatro L01-(P. S! K228-p
PInterUż K01-(p. _D J207.1-pBazDan K04-(n. #Bda G117-n
BazDan K04-(p. #bdA G117-p
PKM K01-(n. KB A227-n
WspInfPM P01-(p. CS A338-p
Inżopr W-(N) #IOP A123-n
Inżopr W-(P) #IOp A123-p
410:00-10:45Mechatro P04-(n. #Mtr K227-n
Proj3D K04-(p. #3D J207.1-p
Proj3D K01-(n. Do J207.1-n
PSieciKP L02-(N. AP G110-n
PSieciKP L02-(P. PA G110-p
Mechatro L01-(P. S! K228-p
PInterUż K01-(p. _D J207.1-pBazDan K04-(n. #Bda G117-n
BazDan K04-(p. #bdA G117-p
PKM K01-(n. KB A227-n
WspInfPM P01-(p. CS A338-p
Inżopr W-(N) #IOP A123-n
Inżopr W-(P) #IOp A123-p
511:00-11:45PAplikIn K04-(n. #pai J209-n
PInterUż K04-(p. #piu J207.1-p
Mechatro P01-(n. TJ K227-n
PAplikIn K01-(p. _W J209-p
PSieciKP L02-(N. AP G110-n
PSieciKP L02-(P. PA G110-p
 BazDan K04-(n. #Bda G117-n
BazDan K04-(p. #bdA G117-p
PKM W-(P) #PKM A124-p
611:45-12:30PAplikIn K04-(n. #pai J209-n
PInterUż K04-(p. #piu J207.1-p
Mechatro P01-(n. TJ K227-n
PAplikIn K01-(p. _W J209-p
PSieciKP L01-(N. AP G110-n
PSieciKP L01-(P. PA G110-p
Inżopr P01-(n. Gj G120-n
Inżopr K01-(p. gJ G120-p
 PKM W-(P) #PKM A124-p
712:45-13:30PrSteroP L01-(n. SP G107-n
PrSteroP L01-(p. sp G107-p
PSieciKP L01-(N. AP G110-n
PSieciKP L01-(P. PA G110-p
Mechatro L02-(N. K228-n
Inżopr P01-(n. Gj G120-n
Inżopr K01-(p. gJ G120-p
Inżopr P04-(n. #ioP G120-n
Inżopr K04-(p. #iop G120-p
WspInfPM W-(N) #WPM A124-n
Mechatron W-(P) #MTR G18-p
813:30-14:15PrSteroP L01-(n. SP G107-n
PrSteroP L01-(p. sp G107-p
PSieciKP L01-(N. AP G110-n
PSieciKP L01-(P. PA G110-p
Mechatro L02-(N. K228-n
Inżopr P01-(n. Gj G120-n
Inżopr K01-(p. gJ G120-p
Inżopr P04-(n. #ioP G120-n
Inżopr K04-(p. #iop G120-p
WspInfPM W-(N) #WPM A124-n
Mechatron W-(P) #MTR G18-p
914:30-15:15PKM K04-(N. #Pkm A227-nPPSystM K04-(n. #Psm J209-n
PPSystM K04-(p. #PSm J209-p
 WspInfPM P04-(N) #Wpm A338-n
PSieciKP W-(P) #PKP A437-p
BazDan W-(N) #BDa G18-n
BazDan W-(P) #bda G18-p
1015:15-16:00PKM K04-(N. #Pkm A227-nPPSystM K04-(n. #Psm J209-n
PPSystM K04-(p. #PSm J209-p
 WspInfPM P04-(N) #Wpm A338-n
PSieciKP W-(P) #PKP A437-p
BazDan W-(N) #BDa G18-n
BazDan W-(P) #bda G18-p
1116:15-17:00PrSteroP L04-(N. #Psp G107-n
PrSteroP L04-(P. #psP G107-p
PPSystM K01-(n. GF J207.1-n
PPSystM K01-(p. FG J207.1-p
 PPSystM W-(N) #PSM G18-n
PAplikInt W-(P) #PAI G18-p
BazDan W-(N) #BDa G18-n
BazDan W-(P) #bda G18-p
1217:00-17:45PrSteroP L04-(N. #Psp G107-n
PrSteroP L04-(P. #psP G107-p
PPSystM K01-(n. GF J207.1-n
PPSystM K01-(p. FG J207.1-p
 PPSystM W-(N) #PSM G18-n
PAplikInt W-(P) #PAI G18-p
 
1318:00-18:45SocPsychP Ć-(N) JJ A409-nBazDan K01-(N) PB G117-n
BazDan K01-(P) BP G117-p
termin dodatkowy Katedra M7termin dodatkowy Katedra M7 
1418:45-19:30SocPsychP Ć-(N) JJ A409-nBazDan K01-(N) PB G117-n
BazDan K01-(P) BP G117-p
termin dodatkowy Katedra M7termin dodatkowy Katedra M7 
1519:45-20:30PrSteroP W-(N) #psp G18-nBazDan K01-(N) PB G117-n
BazDan K01-(P) BP G117-p
   
1620:30-21:15PrSteroP W-(N) #psp G18-n    
Drukuj plan + + + + + + + +
wygenerowano 02.06.2025
za pomocą programu Plan lekcji Optivum
firmy VULCAN
logo programu Plan lekcji Optivum
+
+ + + """; + + String listHTML = """ + + + + + + Lista oddziałów, nauczycieli i sal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Oddziały
+
+

11A1

+

11K2

+

12K1

+

12K2

+

12K3

+
Nauczyciele
+
+
Sale
+
+
+ + + """; + + +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/cache/CacheConfigTest.java b/src/test/java/org/pkwmtt/cache/CacheConfigTest.java index 2327686..6aa8251 100644 --- a/src/test/java/org/pkwmtt/cache/CacheConfigTest.java +++ b/src/test/java/org/pkwmtt/cache/CacheConfigTest.java @@ -1,55 +1,84 @@ package org.pkwmtt.cache; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.pkwmtt.timetable.CacheableTimetableService; -import org.pkwmtt.timetable.dto.TimetableDTO; +import org.pkwmtt.ValuesForTest; +import org.pkwmtt.timetable.TimetableCacheService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import test.TestConfig; -import java.util.ArrayList; -import java.util.List; - +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; - -@SpringBootTest -class CacheConfigTest { +class CacheConfigTest extends TestConfig { @Autowired - private CacheableTimetableService service; + private TimetableCacheService service; @Autowired private CacheManager cacheManager; + @BeforeEach + public void initWireMock() { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); + } + @Test void testCacheKeyPresent_Schedule() { - service.getGeneralGroupSchedule("12K1"); + //given - Cache cache = cacheManager.getCache("timetables"); - assertThat(cache).isNotNull(); - - Cache.ValueWrapper wrapper = cache.get("12K1"); - assertThat(wrapper).isNotNull(); - assertThat(wrapper.get()).isInstanceOf(TimetableDTO.class); + //when + service.getGeneralGroupSchedule("12K1"); + var cache = cacheManager.getCache("timetables"); - TimetableDTO second = service.getGeneralGroupSchedule("12K1"); - assertThat(second).isSameAs(wrapper.get()); + //then + assertAll( + () -> { + assertThat(cache).isNotNull(); + assertThat(cache.get("generalGroupMap", String.class)) + .isEqualTo("{\"11K2\":\"plany/o8.html\",\"12K1\":\"plany/o25.html\",\"11A1\":\"plany/o1.html\",\"12K3\":\"plany/o27.html\",\"12K2\":\"plany/o26.html\"}"); + }, + () -> { + var wrapper = cache.get("timetable_12K1"); + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isInstanceOf(String.class); + } + ); } @Test void testCacheKeyPresent_HoursList(){ - service.getListOfHours(); + //given - Cache cache = cacheManager.getCache("timetables"); - assertThat(cache).isNotNull(); - - Cache.ValueWrapper wrapper = cache.get("hoursList"); - assertThat(wrapper).isNotNull(); - assertThat(wrapper.get()).isInstanceOf(ArrayList.class); + //when + service.getListOfHours(); + var cache = cacheManager.getCache("timetables"); - List second = service.getListOfHours(); - assertThat(second).isSameAs(wrapper.get()); + //then + assertAll( + () -> { + assertThat(cache).isNotNull(); + assertThat(cache.get("hourList", String.class)) + .isEqualTo("[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]"); + }, + () -> { + var wrapper = cache.get("hourList"); + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isInstanceOf(String.class); + } + ); } - } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/cache/CacheInspector.java b/src/test/java/org/pkwmtt/cache/CacheInspector.java similarity index 51% rename from src/main/java/org/pkwmtt/cache/CacheInspector.java rename to src/test/java/org/pkwmtt/cache/CacheInspector.java index 4dd315e..0b38463 100644 --- a/src/main/java/org/pkwmtt/cache/CacheInspector.java +++ b/src/test/java/org/pkwmtt/cache/CacheInspector.java @@ -2,39 +2,48 @@ import com.github.benmanes.caffeine.cache.Cache; import lombok.RequiredArgsConstructor; -import org.pkwmtt.timetable.CacheableTimetableService; +import org.pkwmtt.timetable.TimetableCacheService; import org.springframework.cache.CacheManager; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.stereotype.Component; import java.util.Map; +import static java.util.Objects.isNull; + @Component @RequiredArgsConstructor +@SuppressWarnings("unused") public class CacheInspector { - + private final CacheManager cacheManager; - private final CacheableTimetableService service; - - - public Map getAllEntries(String cacheName) { + private final TimetableCacheService service; + + public Map getAllEntries (String cacheName) { CaffeineCache springCache = (CaffeineCache) cacheManager.getCache(cacheName); - - if (springCache == null) + + if (isNull(springCache)) { throw new IllegalArgumentException("No cache with name " + cacheName); - + } + Cache nativeCache = springCache.getNativeCache(); - + return nativeCache.asMap(); } - - public void printAllEntries(String cacheName) { + + public String printAllEntries (String cacheName) { service.getListOfHours(); service.getGeneralGroupSchedule("12K1"); - service.getGeneralGroupsList(); - - getAllEntries(cacheName).forEach((key, value) -> - System.out.println("Cache[" + cacheName + "] " + key + " -> " + value) - ); + service.getGeneralGroupsMap(); + var s = new StringBuilder(); + getAllEntries(cacheName).forEach((key, value) -> s + .append("Cache[") + .append(cacheName) + .append("] ") + .append(key) + .append(" -> ") + .append(value) + .append("\n")); + return s.toString(); } } diff --git a/src/test/java/org/pkwmtt/cache/CacheInspectorTest.java b/src/test/java/org/pkwmtt/cache/CacheInspectorTest.java deleted file mode 100644 index c6fe172..0000000 --- a/src/test/java/org/pkwmtt/cache/CacheInspectorTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.pkwmtt.cache; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class CacheInspectorTest { - - @Autowired - private CacheInspector inspector; - - @Test - public void inspectCachedData_timetables(){ - inspector.printAllEntries("timetables"); - } - -} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java new file mode 100644 index 0000000..5f7f6ae --- /dev/null +++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java @@ -0,0 +1,620 @@ +package org.pkwmtt.examCalendar; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.pkwmtt.examCalendar.repository.ExamRepository; +import org.pkwmtt.examCalendar.repository.ExamTypeRepository; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * integration tests of ExamCalendar + */ +@SpringBootTest +@AutoConfigureMockMvc +class ExamControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ExamTypeRepository examTypeRepository; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ObjectMapper mapper; + + @BeforeEach + void setupBeforeEach() { + examRepository.deleteAll(); + examTypeRepository.deleteAll(); + } + + // + + /** + * check if addExam endpoint create new exam with correct URI and correct data + */ + @Test + void addExamWithCorrectData() throws Exception { +// given + createExampleExamType("Project"); + ExamDto examDtoRequest = createExampleExamDto("Project"); + String json = mapper.writeValueAsString(examDtoRequest); + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders + .post("/pkwmtt/api/v1/exams") + .contentType("application/json") + .content(json) + ).andDo(print()) + .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(); + + assertEquals(examDtoRequest.getTitle(), examResponse.getTitle()); + assertEquals(examDtoRequest.getDescription(), examResponse.getDescription()); +// compare dates with minutes level precision + assertEquals( + examDtoRequest.getDate().truncatedTo(ChronoUnit.MINUTES), + examResponse.getDate().truncatedTo(ChronoUnit.MINUTES) + ); + assertEquals(examDtoRequest.getExamGroups(), examResponse.getExamGroups()); + assertEquals(examDtoRequest.getExamType(), examResponse.getExamType().getName()); + } + + @Test + void addExamWithBlankExamTitle() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); +// no exam title + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("title : must not be blank", result); + } + + @Test + void addExamWithBlankExamDescription() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); +// no exam description + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// 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 + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); +// no date + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("date : must not be null", result); + } + + @Test + void addExamWithBlankExamGroups() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); +// no examGroups + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("examGroups : must not be blank", result); + } + + @Test + void addExamWithNullExamTypes() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); +// no examType + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("examType : must not be null", result); + } + + @Test + void addExamWithNotFutureDate() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().minusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("date : Date must be in the future", result); + } + + @Test + void addExamWithEmptyStringExamTitle() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", ""); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("title : must not be blank", result); + } + + @Test + void addExamWithEmptyStringExamGroups() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", ""); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("examGroups : must not be blank", result); + } + + @Test + void addExamWithTooLongExamTitle() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("title : max size of field is 255", result); + } + + @Test + void addExamWithTooLongDescription() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("description : max size of field is 255", result); + } + + @Test + void addExamWithTooLongExamGroups() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + requestData.put("examType", "Project"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("examGroups : max size of field is 255", result); + } + + @Test + void addExamWithNonExistingExamType() throws Exception { +// given + createExampleExamType("Project"); + Map requestData = new HashMap<>(); + requestData.put("title", "Math exam"); + requestData.put("description", "first exam"); + requestData.put("date", LocalDateTime.now().plusDays(1).toString()); + requestData.put("examGroups", "12K2, L04"); + requestData.put("examType", "NonExistingExamType"); + +// when + MvcResult result = assertPostRequest(status().isBadRequest(), requestData); + +// then + assertResponseMessage("Invalid exam type NonExistingExamType", result); + } + + + // + + // + @Test + void modifyExamWithCorrectData() throws Exception { +// given + ExamType examType = createExampleExamType("Exam"); + Exam exam = createExampleExam(examType); + int id = examRepository.save(exam).getExamId(); + ExamDto examDto = createExampleExamDto(examType.getName()); + +// when + assertPutRequest(status().isNoContent(), examDto, id); + +// then + Exam responseExam = examRepository.findById(id).orElseThrow(); + assertEquals("Math exam", responseExam.getTitle()); + assertEquals("first exam", responseExam.getDescription()); + assertEquals( + LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES), + responseExam.getDate().truncatedTo(ChronoUnit.MINUTES) + ); + assertEquals("12K2, L04", responseExam.getExamGroups()); + } + + @Test + void modifyExamWithIncorrectExamId() throws Exception { +// given + ExamType examType = createExampleExamType("Exam"); + 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 { +// given + 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 + ExamType examType = createExampleExamType("Exam"); + Exam exam = createExampleExam(examType); + 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()); + assertEquals( + exam.getDate().truncatedTo(ChronoUnit.MINUTES), + LocalDateTime.parse(responseNode.get("date").textValue()).truncatedTo(ChronoUnit.MINUTES) + ); + assertEquals(exam.getExamGroups(), responseNode.get("examGroups").asText()); + assertEquals(mapper.readTree(mapper.writeValueAsString(exam.getExamType())), responseNode.get("examType")); + } + + @Test + void getNonExistingExamById() throws Exception { +// given + ExamType examType = createExampleExamType("Exam"); + Exam exam = createExampleExam(examType); + 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 getExams() { +// TODO: test getExamsByGroups after implementing new version + } + + // + + @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 +// when + MvcResult result = mockMvc.perform(MockMvcRequestBuilders + .get("/pkwmtt/api/v1/exams/exam-types") + .contentType("application/json") + ).andDo(print()) + .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 + * @param name of new examType + * @return created examType object + */ + private ExamType createExampleExamType(String name) { + ExamType examType = ExamType.builder().name(name).build(); + 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 + */ + private Exam createExampleExam(ExamType type) { + return Exam.builder() + .title("Exam") + .description("Exam description") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("11K1, L01") + .examType(type) + .build(); + } + + /** + * @param examTypeName name of type of exam as String + * @return created ExamDto + */ + private ExamDto createExampleExamDto(String examTypeName) { + return new ExamDto( + "Math exam", + "first exam", + LocalDateTime.now().plusDays(1), + "12K2, L04", + examTypeName + ); + } + + /** + * compare error message form response with expected value + * @param expectedMessage full message that is expected in response + * @param result response generated by mockMvc.perform() or one of assert[httpMethod]Request() + * @throws Exception + */ + private void assertResponseMessage(String expectedMessage, MvcResult result) throws Exception { + JsonNode jsonResponse = mapper.readTree(result.getResponse().getContentAsString()); + 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 + * @param expectedStatus status().[http response] (example: status().isCreated() ) + * @param content object that would be mapped to JSON by ObjectMapper and then attached to request + * it could be dto object or Map + * @return MvcResult object which could be used to capture response body + * @throws Exception + */ + private MvcResult assertPostRequest(ResultMatcher expectedStatus, Object content) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .post("/pkwmtt/api/v1/exams") + .contentType("application/json") + .content(mapper.writeValueAsString(content)) + ).andDo(print()) + .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 + * @param expectedStatus status().[http response] (example: status().isNoContent() ) + * @param content object that would be mapped to JSON by ObjectMapper and then attached to request + * @param pathId id of resource that would be updated + * @return MvcResult object which could be used to capture response body + * @throws Exception + */ + private MvcResult assertPutRequest(ResultMatcher expectedStatus, Object content, int pathId) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .put("/pkwmtt/api/v1/exams/{id}", pathId) + .contentType("application/json") + .content(mapper.writeValueAsString(content)) + ).andDo(print()) + .andExpect(expectedStatus) + .andReturn(); + } + + /** + * method send DELETE request to ExamController with examId as pathID. + * Then check if response code is the same as expected + * @param expectedStatus status().[http response] (example: status().isNoContent() ) + * @param pathId id of resource that would be deleted + * @return MvcResult object which could be used to capture response body + * @throws Exception + */ + private MvcResult assertDeleteRequest(ResultMatcher expectedStatus, int pathId) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .delete("/pkwmtt/api/v1/exams/{id}", pathId) + .contentType("application/json") + ).andDo(print()) + .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 + * @param expectedStatus status().[http response] (example: status().isOk() ) + * @param pathId id of resource that would be returned + * @return MvcResult object which could be used to capture response body + * @throws Exception + */ + private MvcResult assertGetByIdRequest(ResultMatcher expectedStatus, int pathId) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .get("/pkwmtt/api/v1/exams/{id}", pathId) + .contentType("application/json") + ).andDo(print()) + .andExpect(expectedStatus) + .andReturn(); + } + + /** + * method send GET request to ExamController at /pkwmtt/api/v1/exams/exam-types URI. + * Then check if response code is the same as expected + * @param expectedStatus expectedStatus status().[http response] (example: status().isOk() ) + * @return MvcResult object which could be used to capture response body + * @throws Exception + */ + private MvcResult assertGetExamTypesRequest(ResultMatcher expectedStatus) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .get("/pkwmtt/api/v1/exams/exam-types") + .contentType("application/json") + ).andDo(print()) + .andExpect(expectedStatus) + .andReturn(); + } + +// + +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java new file mode 100644 index 0000000..c86b0ab --- /dev/null +++ b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java @@ -0,0 +1,331 @@ +package org.pkwmtt.examCalendar; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.pkwmtt.examCalendar.mapper.ExamDtoToExamMapper; +import org.pkwmtt.examCalendar.repository.ExamRepository; +import org.pkwmtt.exceptions.UnsupportedCountOfArgumentsException; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ExamServiceTest { + + @Mock + private ExamRepository examRepository; + + @Mock + private ExamDtoToExamMapper examDtoToExamMapper; + + @InjectMocks + private ExamService examService; + + @Test + void addExam() { +// given + int examId = 1; + ExamDto examDto = new ExamDto( + "Math exam", + "desc", + LocalDateTime.now().plusDays(1), + "12K2, 13L1", + "Exam" + ); + Exam exam = Exam.builder() + .title("Math exam") + .description("desc") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, 13L1") + .examType(new ExamType(1, "Exam")) + .build(); + when(examDtoToExamMapper.mapToNewExam(examDto)).thenReturn(exam); + +// assign exam id in repository + when(examRepository.save(exam)).thenAnswer(invocation -> { + Exam newExam = invocation.getArgument(0, Exam.class); + Field field = Exam.class.getDeclaredField("examId"); + field.setAccessible(true); + field.set(newExam, examId); + return newExam; + }); +// when + int result = examService.addExam(examDto); +// then + assertEquals(examId, result); + verify(examRepository).save(exam); + verify(examDtoToExamMapper).mapToNewExam(examDto); + } + + /************************************************************************************/ +//modify exam + @Test + void shouldModifyExamWhenIdExists() { + // given + int examId = 1; + ExamDto examDto = mock(ExamDto.class); + Exam exam = mock(Exam.class); + + when(examDtoToExamMapper.mapToExistingExam(examDto, examId)).thenReturn(exam); + when(examRepository.findById(examId)).thenReturn(Optional.of(exam)); +// when + examService.modifyExam(examDto, examId); +// then + verify(examDtoToExamMapper).mapToExistingExam(examDto, examId); + verify(examRepository).save(exam); + } + + @Test + void shouldThrowWhenExamIdNotExists() { + // given + int examId = 5; + ExamDto examDto = mock(ExamDto.class); + when(examRepository.findById(examId)).thenThrow(new NoSuchElementException("Exam not found")); +// when + RuntimeException exception = assertThrows( + NoSuchElementException.class, + () -> examService.modifyExam(examDto, examId) + ); +// then + verify(examDtoToExamMapper, never()).mapToExistingExam(examDto, examId); + verify(examRepository, never()).save(any()); + assertEquals("Exam not found", exception.getMessage()); + } + + /************************************************************************************/ +//delete exam + @Test + void shouldDeleteExamWhenIdExists() { +// given + int examId = 1; + when(examRepository.findById(examId)).thenReturn(Optional.of(mock(Exam.class))); +// when + examService.deleteExam(examId); +// then + verify(examRepository).deleteById(examId); + } + + @Test + void shouldThrowExceptionWhenExamIdNotExists() { +// given + int examId = 5; + when(examRepository.findById(examId)).thenThrow(new NoSuchElementException("Exam not found")); +// when + RuntimeException exception = assertThrows( + NoSuchElementException.class, + () -> examService.deleteExam(examId) + ); +// then + verify(examRepository, never()).deleteById(examId); + assertEquals("Exam not found", exception.getMessage()); + } + + /************************************************************************************/ +// getExamById + @Test + void getExamById() { +// given + int examId = 1; + when(examRepository.findById(examId)).thenReturn(Optional.of(mock(Exam.class))); +// when + Exam exam = examService.getExamById(examId); +// then + verify(examRepository).findById(examId); + assertNotNull(exam); + } + + @Test + void shouldThrowExceptionWhenExamNotFound() { +// given + int examId = 5; + when(examRepository.findById(examId)).thenThrow(new NoSuchElementException("Exam not found")); +// when + RuntimeException exception = assertThrows( + NoSuchElementException.class, + () -> examService.getExamById(examId) + ); +// then + assertEquals("Exam not found", exception.getMessage()); + } + + // getExamByGroup + @Test + void shouldThrowWithMoreThan4Arguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + groups.add("13L1"); + groups.add("13A2"); + groups.add("41S2"); + groups.add("11S3"); +// when + RuntimeException exception = assertThrows( + UnsupportedCountOfArgumentsException.class, + () -> examService.getExamByGroup(groups) + ); +// then + assertEquals( + "Invalid count of arguments provided: 5 expected more than: 1 less than: 5", + exception.getMessage() + ); + } + + + @Test + void shouldCallRepositoryWith4Arguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + groups.add("13L1"); + groups.add("13A2"); + groups.add("41S2"); + Exam mockExam = mock(Exam.class); + Set exams = new HashSet<>(); + exams.add(mockExam); + when(examRepository.findExamsByGroupsIdentifier(any(), any(), any(), any())).thenReturn(exams); +// when + Set result = examService.getExamByGroup(groups); +// then + List> cap = new ArrayList<>(); + for (int i = 0; i < 4; ++i) + cap.add(ArgumentCaptor.forClass(String.class)); + + verify(examRepository).findExamsByGroupsIdentifier( + cap.get(0).capture(), + cap.get(1).capture(), + cap.get(2).capture(), + cap.get(3).capture() + ); + Set passedGroups = cap.stream().map(ArgumentCaptor::getValue).collect(Collectors.toSet()); + + assertEquals(groups, passedGroups); + assertEquals(exams, result); + } + + + @Test + void shouldCallRepositoryWith3Arguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + groups.add("13L1"); + groups.add("13A2"); + Exam mockExam = mock(Exam.class); + Set exams = new HashSet<>(); + exams.add(mockExam); + when(examRepository.findExamsByGroupsIdentifier(any(), any(), any())).thenReturn(exams); +// when + Set result = examService.getExamByGroup(groups); +// then + List> cap = new ArrayList<>(); + for (int i = 0; i < 3; ++i) + cap.add(ArgumentCaptor.forClass(String.class)); + + verify(examRepository).findExamsByGroupsIdentifier( + cap.get(0).capture(), + cap.get(1).capture(), + cap.get(2).capture() + ); + Set passedGroups = cap.stream().map(ArgumentCaptor::getValue).collect(Collectors.toSet()); + + assertEquals(groups, passedGroups); + assertEquals(exams, result); + } + + @Test + void shouldCallRepositoryWith2Arguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + groups.add("13L1"); + Exam mockExam = mock(Exam.class); + Set exams = new HashSet<>(); + exams.add(mockExam); + when(examRepository.findExamsByGroupsIdentifier(any(), any())).thenReturn(exams); +// when + Set result = examService.getExamByGroup(groups); +// then + List> cap = new ArrayList<>(); + for (int i = 0; i < 2; ++i) + cap.add(ArgumentCaptor.forClass(String.class)); + + verify(examRepository).findExamsByGroupsIdentifier( + cap.get(0).capture(), + cap.get(1).capture() + ); + Set passedGroups = cap.stream().map(ArgumentCaptor::getValue).collect(Collectors.toSet()); + + assertEquals(groups, passedGroups); + assertEquals(exams, result); + } + + @Test + void shouldCallRepositoryWithSingleArguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + Exam mockExam = mock(Exam.class); + Set exams = new HashSet<>(); + exams.add(mockExam); + when(examRepository.findExamsByGroupsIdentifier(any())).thenReturn(exams); +// when + Set result = examService.getExamByGroup(groups); +// then + ArgumentCaptor cap = ArgumentCaptor.forClass(String.class); + + verify(examRepository).findExamsByGroupsIdentifier(cap.capture()); + Set passedGroups = new HashSet<>(); + passedGroups.add(cap.getValue()); + + assertEquals(groups, passedGroups); + assertEquals(exams, result); + } + + + @Test + void shouldCallRepositoryWithDuplicatesOf4UniqueArguments() { +// given + Set groups = new HashSet<>(); + groups.add("12K2"); + groups.add("13L1"); + groups.add("13A2"); + groups.add("41S2"); + groups.add("41S2"); + groups.add("13L1"); + Exam mockExam = mock(Exam.class); + Set exams = new HashSet<>(); + exams.add(mockExam); + when(examRepository.findExamsByGroupsIdentifier(any(), any(), any(), any())).thenReturn(exams); +// when + Set result = examService.getExamByGroup(groups); +// then + List> cap = new ArrayList<>(); + for (int i = 0; i < 4; ++i) + cap.add(ArgumentCaptor.forClass(String.class)); + + verify(examRepository).findExamsByGroupsIdentifier( + cap.get(0).capture(), + cap.get(1).capture(), + cap.get(2).capture(), + cap.get(3).capture() + ); + Set passedGroups = cap.stream().map(ArgumentCaptor::getValue).collect(Collectors.toSet()); + + assertEquals(groups, passedGroups); + assertEquals(exams, result); + assertEquals(4, passedGroups.size()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java new file mode 100644 index 0000000..b14edc6 --- /dev/null +++ b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java @@ -0,0 +1,150 @@ +package org.pkwmtt.examCalendar.dto; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ExamDtoTest { + + private final Validator validator; + + public ExamDtoTest() { + this.validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Mock + private ExamDto examDto; + + @Test + void validData() { +// given + ExamDto examDto = new ExamDto( + "Math exam", + "First exam", + LocalDateTime.now().plusDays(1), + "12K2, K04", + "exam" + ); +// when, then + assertTrue(validator.validate(examDto).isEmpty()); + } + + + // empty Strings + @Test + void emptyStringTitle() { + // given + ExamDto examDto = new ExamDto( + "", + "First exam", + LocalDateTime.now().plusDays(1), + "12K2, K04", + "exam" + ); +// when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("title"))); + } + + @Test + void emptyExamGroups() { + // given + ExamDto examDto = new ExamDto( + "Math exam", + "First exam", + LocalDateTime.now().plusDays(1), + "", + "exam" + ); +// when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("examGroups"))); + } + +// to long Strings + + @Test + void toLongStringTitle() { + // given + ExamDto examDto = new ExamDto( +// 256 characters + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "First exam", + LocalDateTime.now().plusDays(1), + "12K2, K04", + "exam" + ); +// when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("title"))); + } + + @Test + void toLongDescription() { + // given + ExamDto examDto = new ExamDto( + "Math exam", +// 256 characters + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + LocalDateTime.now().plusDays(1), + "12K2, K04", + "exam" + ); +// when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("description"))); + } + + @Test + void toLongExamGroups() { + // given + ExamDto examDto = new ExamDto( + "Math exam", + "First exam", + LocalDateTime.now().plusDays(1), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "exam" + ); +// when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("examGroups"))); + } + +// date not in future + + @Test + void dateNotInFuture() { + // given + ExamDto examDto = new ExamDto( + "Math exam", + "First exam", + LocalDateTime.now().minusHours(1), + "12K2, K04", + "exam" + ); + // when + Set> violations = validator.validate(examDto); +// then + assertFalse(validator.validate(examDto).isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("date"))); + } + +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapperTest.java b/src/test/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapperTest.java new file mode 100644 index 0000000..45081ae --- /dev/null +++ b/src/test/java/org/pkwmtt/examCalendar/mapper/ExamDtoToExamMapperTest.java @@ -0,0 +1,150 @@ +package org.pkwmtt.examCalendar.mapper; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pkwmtt.examCalendar.dto.ExamDto; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.pkwmtt.examCalendar.repository.ExamTypeRepository; +import org.pkwmtt.exceptions.InvalidGroupIdentifierException; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExamDtoToExamMapperTest { + + @Mock + private ExamTypeRepository examTypeRepository; + + @InjectMocks + private ExamDtoToExamMapper examDtoToExamMapper; + + private ExamDto examDto; + private String examTypeName; + +// @BeforeEach +// void setup() { +// +// } + + /**********************************************************************************/ +// mapToNewExam + @Test + void isFieldsMappedProperlyToNewExam() { +// given + String examTypeName = "exam"; + ExamDto examDto = new ExamDto( + "Math exam", + "Linear algebra", + LocalDateTime.now().plusDays(1), + "12K2, 13S1", + examTypeName + ); + when(examTypeRepository.findByName(examTypeName)).thenReturn( + Optional.of(ExamType.builder() + .name(examTypeName) + .build()) + ); +// when + Exam exam = examDtoToExamMapper.mapToNewExam(examDto); +// then +// test fields + assertEquals(examDto.getTitle(), exam.getTitle()); + assertEquals(examDto.getDescription(), exam.getDescription()); + assertEquals(examDto.getDate(), exam.getDate()); + assertEquals(examDto.getExamGroups(), exam.getExamGroups()); + assertEquals(examTypeName, exam.getExamType().getName()); +// test null id + assertNull(exam.getExamId()); + } + + @Test + void ShouldThrowExceptionWhenGroupIdentifierIsLongerThanSixCharactersForNewExam() { + // given + String examTypeName = "exam"; + ExamDto examDto = new ExamDto( + "Math exam", + "Linear algebra", + LocalDateTime.now().plusDays(1), + "12K2, 13S1, Not_Valid_Identifier, 41K1", + examTypeName + ); + when(examTypeRepository.findByName(examTypeName)).thenReturn( + Optional.of(ExamType.builder() + .name(examTypeName) + .build()) + ); +// then + RuntimeException exception = assertThrows( + InvalidGroupIdentifierException.class, + () -> examDtoToExamMapper.mapToNewExam(examDto) + ); + assertEquals("Invalid group identifier: Not_Valid_Identifier", exception.getMessage()); + } + + + /**********************************************************************************/ +// mapToExistingExam + @Test + void isFieldsMappedProperlyToExistingExam() { + // given + int examId = 1; + examTypeName = "exam"; + examDto = new ExamDto( + "Math exam", + "Linear algebra", + LocalDateTime.now().plusDays(1), + "12K2, 13S1", + examTypeName + ); + when(examTypeRepository.findByName(examTypeName)).thenReturn( + Optional.of(ExamType.builder() + .name(examTypeName) + .build()) + ); +// when + Exam exam = examDtoToExamMapper.mapToExistingExam(examDto, examId); +// then +// test fields + assertEquals(examId, exam.getExamId()); + assertEquals(examDto.getTitle(), exam.getTitle()); + assertEquals(examDto.getDescription(), exam.getDescription()); + assertEquals(examDto.getDate(), exam.getDate()); + assertEquals(examDto.getExamGroups(), exam.getExamGroups()); + assertEquals(examTypeName, exam.getExamType().getName()); +// test not null id + assertNotNull(exam.getExamId()); + } + + @Test + void ShouldThrowExceptionWhenGroupIdentifierIsLongerThanSixCharactersForExistingExam() { + // given + int examId = 1; + String examTypeName = "exam"; + ExamDto examDto = new ExamDto( + "Math exam", + "Linear algebra", + LocalDateTime.now().plusDays(1), + "12K2, 13S1, Not_Valid_Identifier, 41K1", + examTypeName + ); + when(examTypeRepository.findByName(examTypeName)).thenReturn( + Optional.of(ExamType.builder() + .name(examTypeName) + .build()) + ); +// then + RuntimeException exception = assertThrows( + InvalidGroupIdentifierException.class, + () -> examDtoToExamMapper.mapToExistingExam(examDto, examId) + ); + assertEquals("Invalid group identifier: Not_Valid_Identifier", exception.getMessage()); + } +} diff --git a/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java new file mode 100644 index 0000000..90f1614 --- /dev/null +++ b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java @@ -0,0 +1,182 @@ +package org.pkwmtt.examCalendar.repository; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pkwmtt.examCalendar.entity.Exam; +import org.pkwmtt.examCalendar.entity.ExamType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +@DataJpaTest +class ExamRepositoryTest { + + @Autowired + private ExamRepository examRepository; + + @Autowired + private ExamTypeRepository examTypeRepository; + + private ExamType examType; + + @BeforeEach + void setup(){ + examType = ExamType.builder() + .name("exam") + .build(); + examTypeRepository.save(examType); + } + + /** + * test if method find specific count of exams when 1 or 0 group identifiers match + */ + @Test + void testSingleIdentifierMatch() { +// given + Exam exam1 = Exam.builder() + .title("Exam 1") + .description("Exam 1") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, K03") + .examType(examType) + .build(); + examRepository.save(exam1); + Exam exam2 = Exam.builder() + .title("Exam 2") + .description("Exam 2") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K3, K03, S02") + .examType(examType) + .build(); + examRepository.save(exam2); + Exam exam3 = Exam.builder() + .title("Exam 3") + .description("Exam 3") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("13K1, K05, L05") + .examType(examType) + .build(); + examRepository.save(exam3); + Exam exam4 = Exam.builder() + .title("Exam 4") + .description("Exam 4") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("41K1, L04, P03, I01") + .examType(examType) + .build(); + examRepository.save(exam4); + Exam exam5 = Exam.builder() + .title("Exam 5") + .description("Exam 5") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("11A1, G03, H01, P02") + .examType(examType) + .build(); + examRepository.save(exam5); + + String generalGroup = "12K2"; + String kGroup = "K05"; + String lGroup = "L04"; + String pGroup = "P02"; + +// when + Set exams = examRepository.findExamsByGroupsIdentifier(generalGroup, kGroup, lGroup, pGroup); + List examsTitles = exams.stream().map(Exam::getTitle).toList(); +// then + assertEquals(4, exams.size()); + assertTrue(examsTitles.contains("Exam 1")); + assertTrue(examsTitles.contains("Exam 3")); + assertTrue(examsTitles.contains("Exam 4")); + assertTrue(examsTitles.contains("Exam 5")); + } + + /** + * test if method don't duplicate exams when more than 1 identifier match + */ + @Test + void testMultipleIdentifierMatch() { +// given + Exam exam1 = Exam.builder() + .title("Exam 1") + .description("Exam 1") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, K01, L04, P03, I01") + .examType(examType) + .build(); + examRepository.save(exam1); + Exam exam2 = Exam.builder() + .title("Exam 2") + .description("Exam 2") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, K05, L04, P02") + .examType(examType) + .build(); + examRepository.save(exam2); + Exam exam3 = Exam.builder() + .title("Exam 3") + .description("Exam 3") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, K05, L04, P02, I05") + .examType(examType) + .build(); + examRepository.save(exam3); + + String generalGroup = "12K2"; + String kGroup = "K05"; + String lGroup = "L04"; + String pGroup = "P02"; + +// when + Set exams = examRepository.findExamsByGroupsIdentifier(generalGroup, kGroup, lGroup, pGroup); + List examsTitles = exams.stream().map(Exam::getTitle).toList(); + +// then + assertEquals(3, exams.size()); + assertTrue(examsTitles.contains("Exam 1")); + assertTrue(examsTitles.contains("Exam 2")); + assertTrue(examsTitles.contains("Exam 3")); + } + + /** + * test if method return empty set identifiers don't match + */ + @Test + void testNothingMatch() { +// given + Exam exam1 = Exam.builder() + .title("Exam 1") + .description("Exam 1") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K2, K01,") + .examType(examType) + .build(); + examRepository.save(exam1); + Exam exam2 = Exam.builder() + .title("Exam 2") + .description("Exam 2") + .date(LocalDateTime.now().plusDays(1)) + .examGroups("12K3, L05") + .examType(examType) + .build(); + examRepository.save(exam2); + + String generalGroup = "14K3"; + String kGroup = "K05"; + String lGroup = "L02"; + String pGroup = "P02"; + +// when + Set exams = examRepository.findExamsByGroupsIdentifier(generalGroup, kGroup, lGroup, pGroup); + +// then + assertTrue(exams.isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/CacheableTimetableServiceTest.java b/src/test/java/org/pkwmtt/timetable/CacheableTimetableServiceTest.java deleted file mode 100644 index 6781569..0000000 --- a/src/test/java/org/pkwmtt/timetable/CacheableTimetableServiceTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.pkwmtt.timetable; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@SpringBootTest -class CacheableTimetableServiceTest { - @Autowired - CacheableTimetableService service; - - @Test - public void checkIfHoursListBodyIsNotNull() { - var response = service.getListOfHours(); - - assertNotNull(response); - assertFalse(response.isEmpty()); - } - - @Test - public void checkIfGeneralGroupListIsNotNull() { - var response = service.getGeneralGroupsList(); - assertNotNull(response); - assertFalse(response.isEmpty()); - } - -} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java new file mode 100644 index 0000000..f25888f --- /dev/null +++ b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java @@ -0,0 +1,143 @@ +package org.pkwmtt.timetable; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pkwmtt.ValuesForTest; +import org.pkwmtt.cache.CacheInspector; +import org.pkwmtt.timetable.dto.TimetableDTO; +import org.springframework.beans.factory.annotation.Autowired; +import test.TestConfig; + +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +class TimetableCacheServiceTest extends TestConfig { + @Autowired + TimetableCacheService cachedService; + + @Autowired + TimetableService service; + + @Autowired + CacheInspector cacheInspector; + + @BeforeEach + public void initWireMock() { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); + } + + @Test + public void shouldHourListBePresentInCache () { + //given + var key = "hourList"; + cachedService.getListOfHours(); // call method to save data in cache + + //when + Map cacheData = cacheInspector.getAllEntries("timetables"); // get all keys saved in cache + + //then + assertAll( + () -> assertNotNull(cacheData), + () -> assertTrue(cacheData.containsKey(key)), + () -> { + var hourList = cacheData.get(key); + assertNotNull(hourList); + assertThat(hourList).isEqualTo("[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]"); + } + ); + } + + + @Test + public void shouldReturnGeneralGroupsMap () { + //given + var expectedMap = Map.of( + "11K2", "plany/o8.html", + "12K1", "plany/o25.html", + "11A1", "plany/o1.html", + "12K3", "plany/o27.html", + "12K2", "plany/o26.html" + ); + + //when + var result = cachedService.getGeneralGroupsMap(); + + //then + assertThat(result).isEqualTo(expectedMap); + } + + @Test + public void shouldGeneralGroupMapBePresentInCache () { + //given + var key = "generalGroupMap"; + var expectedValue = "{\"11K2\":\"plany/o8.html\",\"12K1\":\"plany/o25.html\",\"11A1\":\"plany/o1.html\",\"12K3\":\"plany/o27.html\",\"12K2\":\"plany/o26.html\"}"; + cachedService.getGeneralGroupsMap(); // call method to save data in cache + + //when + Map cacheData = cacheInspector.getAllEntries("timetables"); // get all keys saved in cache + + //then + assertAll( + () -> assertNotNull(cacheData), + () -> { + assertTrue(cacheData.containsKey(key)); + var data = cacheData.get(key); + assertThat(data).isEqualTo(expectedValue); + } + ); + } + + @Test + @Disabled("Test shouldn't be random") + public void shouldReturnRandomGeneralGroupSchedule () { + //given + List generalGroupList = service.getGeneralGroupList(); + var generalGroupName = generalGroupList.get((int) (Math.random() * generalGroupList.size())); // get random general group + + //when + var result = cachedService.getGeneralGroupSchedule(generalGroupName); + + //then + assertNotNull(result); + assertInstanceOf(TimetableDTO.class, result); + } + + @Test + @Disabled("Test shouldn't be random") + public void shouldRandomGeneralGroupScheduleBePresentInCache () { + //given + List generalGroupList = service.getGeneralGroupList(); + + String generalGroupName = generalGroupList.get((int) (Math.random() * generalGroupList.size())); // get random general group + String key = "timetable_" + generalGroupName; + + cachedService.getGeneralGroupSchedule(generalGroupName); // call method to save data in cache + + //when + Map cacheData = cacheInspector.getAllEntries("timetables"); // get all keys saved in cache + + //then + assertNotNull(cacheData); + assertTrue(cacheData.containsKey(key)); + assertInstanceOf(String.class, cacheData.get(key)); + } +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java b/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java index 42339bf..9c5c290 100644 --- a/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java +++ b/src/test/java/org/pkwmtt/timetable/TimetableControllerTest.java @@ -1,97 +1,189 @@ package org.pkwmtt.timetable; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.pkwmtt.exceptions.ErrorResponseDTO; +import org.pkwmtt.ValuesForTest; +import org.pkwmtt.exceptions.dto.ErrorResponseDTO; import org.pkwmtt.timetable.dto.TimetableDTO; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import test.TestConfig; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; @Slf4j -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class TimetableControllerTest { - +class TimetableControllerTest extends TestConfig { + @LocalServerPort private int port; - + @Autowired private TestRestTemplate restTemplate; + @BeforeEach + public void initWireMock() { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); + } + @Test - public void testGetGeneralGroupScheduleFiltered_withOptionalParams() { - String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/12K1?sub=K01&sub=L01&sub=P01", port); - + public void testGetGeneralGroupScheduleFiltered_withOptionalParams () { + //given + var url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/12K1?sub=K01&sub=L01&sub=P01", + port + ); + + //when ResponseEntity response = restTemplate.getForEntity(url, TimetableDTO.class); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertEquals(5, response.getBody().getData().size()); - assertEquals(12, response.getBody().getData().getFirst().getOdd().size()); - assertEquals(6, response.getBody().getData().getFirst().getEven().size()); + + //then + assertAll( + () -> assertEquals(HttpStatus.OK, response.getStatusCode()), + () -> { + var responseBody = response.getBody(); + assertNotNull(responseBody); + }, + () -> { + var responseData = response.getBody().getData(); + assertEquals(5, responseData.size()); + assertEquals(12, responseData.getFirst().getOdd().size()); + assertEquals(6, responseData.getFirst().getEven().size()); + } + ); } - + @Test - public void testGetGeneralGroupScheduleFiltered_withoutParams() throws JsonProcessingException { - String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/12K1", port); - + public void testGetGeneralGroupScheduleFiltered_withoutParams () { + //given + var url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/12K1", port); + + //when ResponseEntity response = restTemplate.getForEntity(url, TimetableDTO.class); - + + //then assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - ObjectMapper mapper = new ObjectMapper(); - - var result = mapper.writeValueAsString(response.getBody()); - System.out.println(result); - + + assertEquals(5, response.getBody().getData().size()); // 5 days a week } - + @Test - public void shouldReturnListOfGeneralGroups() { - String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/groups/general", port); - - ResponseEntity response = restTemplate.getForEntity(url, String[].class); + public void shouldReturnListOfGeneralGroups () { + //given + String url = String.format( + "http://localhost:%s/pkmwtt/api/v1/timetables/groups/general", + port + ); + + //when + ResponseEntity> response = restTemplate.exchange( + url, HttpMethod.GET, null, new ParameterizedTypeReference<>() { + } + ); + + //then assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); - - List result = Arrays.asList(response.getBody()); - assertNotNull(result); - result.forEach(System.out::println); + assertFalse(response.getBody().isEmpty()); } - + @Test - public void shouldReturnListOfSubgroupsForGeneralGroup() { - String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/groups/12K1", port); - + public void shouldReturnListOfSubgroupsForGeneralGroup () { + //given + String url = String.format( + "http://localhost:%s/pkmwtt/api/v1/timetables/groups/12K1", + port + ); + + //when ResponseEntity response = restTemplate.getForEntity(url, String[].class); - - System.out.println(Arrays.toString(response.getBody())); + + //then assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); } - + @Test - public void textException_SpecifiedGeneralGroupDoesntExistsException() { - String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/groups/XXXX", port); - ResponseEntity response = restTemplate.getForEntity(url, ErrorResponseDTO.class); - + public void shouldReturn_BadRequest_SpecifiedGeneralGroupDoesntExistsException () { + //given + String url = String.format( + "http://localhost:%s/pkmwtt/api/v1/timetables/groups/XXXX", + port + ); + + //when + ResponseEntity response = restTemplate.getForEntity( + url, + ErrorResponseDTO.class + ); + + //then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertNotNull(response.getBody()); - assertThat(response.getBody().getMessage()).contains("Specified general group doesn't exists"); assertThat(response.getBody().getTimestamp()).isBefore(LocalDateTime.now()); } - + + @Test + public void shouldReturn_BadRequest_SpecifiedSubGroupDoesntExistsException () { + //given + String url = String.format( + "http://localhost:%s/pkmwtt/api/v1/timetables/12K1?sub=XXX", + port + ); + + //when + ResponseEntity response = restTemplate.getForEntity( + url, + ErrorResponseDTO.class + ); + + //then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertNotNull(response.getBody()); + assertThat(response.getBody().getTimestamp()).isBefore(LocalDateTime.now()); + } + + @Test + public void shouldReturn_ListOfHours () { + //given + String url = String.format("http://localhost:%s/pkmwtt/api/v1/timetables/hours", port); + + //when + ResponseEntity response = restTemplate.getForEntity(url, String[].class); + + //then + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + + String regex = "\\b(?:[0-9]|1[0-9]|2[0-3]):[0-5][0-9]-(?:[0-9]|1[0-9]|2[0-3]):[0-5][0-9]\\b"; + Pattern pattern = Pattern.compile(regex); + Arrays.stream(response.getBody()).toList().forEach(item -> { + Matcher matcher = pattern.matcher(item); + if(!matcher.find()) fail("Wrong hour format"); + }); + } } \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java b/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java new file mode 100644 index 0000000..c582a3f --- /dev/null +++ b/src/test/java/org/pkwmtt/timetable/TimetableServiceTest.java @@ -0,0 +1,113 @@ +package org.pkwmtt.timetable; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pkwmtt.ValuesForTest; +import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException; +import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException; +import org.springframework.beans.factory.annotation.Autowired; +import test.TestConfig; + +import java.util.List; +import java.util.regex.Pattern; + +class TimetableServiceTest extends TestConfig { + + @Autowired + private TimetableService service; + + @BeforeEach + public void initWireMock() { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); + } + + @Test + public void shouldReturnAvailableSubGroups () throws JsonProcessingException { + //given + var generalGroupName = "12K1"; + var regex = "^[A-Z]\\d{2}$"; + var pattern = Pattern.compile(regex); + var expectedResult = List.of("K01", "K04", "L01", "L02", "L04", "P01", "P04"); + + //when + var result = service.getAvailableSubGroups(generalGroupName); + + //then + assertThat(result).isEqualTo(expectedResult); + + //I don't know why it is in test. I think it is for debug? +// result.forEach(item -> { +// Matcher matcher = pattern.matcher(item); +// if (!matcher.find()) { +// fail("Wrong subgroup format"); +// } +// }); + } + + + @Test + public void shouldThrow_SpecifiedGeneralGroupDoesntExistsException () { + //given + var subgroups = List.of("K01", "L01"); + var generalGroupName = "77Z3"; + //when + + //then + assertThrows( + SpecifiedGeneralGroupDoesntExistsException.class, + () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups) + ); + } + + @Test + public void shouldThrow_SpecifiedSubGroupDoesntExistsException () { + //given + List subgroups = List.of("Z01", "XCD"); + String generalGroupName = "12K1"; + //when + + //then + assertThrows( + SpecifiedSubGroupDoesntExistsException.class, + () -> service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups) + ); + } + + @Test + public void shouldReturnSortedGeneralGroupList () { + //given + var expectedResult = List.of( + "11A1", + "11K2", + "12K1", + "12K2", + "12K3" + ); + //when + var result = service.getGeneralGroupList(); + + //then + assertAll( + () -> assertNotNull(result), + () -> assertFalse(result.isEmpty()), + () -> assertEquals(expectedResult, result) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/pkwmtt/timetable/parser/ParserServiceTest.java b/src/test/java/org/pkwmtt/timetable/parser/ParserServiceTest.java index 063db82..9dd2567 100644 --- a/src/test/java/org/pkwmtt/timetable/parser/ParserServiceTest.java +++ b/src/test/java/org/pkwmtt/timetable/parser/ParserServiceTest.java @@ -2,41 +2,64 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runners.Suite; +import org.pkwmtt.ValuesForTest; import org.pkwmtt.timetable.dto.TimetableDTO; -import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.test.context.support.WithMockUser; +import test.TestConfig; import java.io.IOException; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -@SpringBootTest @Suite.SuiteClasses(TimetableParserService.class) -class ParserServiceTest { +class ParserServiceTest extends TestConfig { TimetableParserService parserService; { parserService = new TimetableParserService(); } + @Value("${main.url}") + private String mainUrl; + + @BeforeEach + public void initWireMock() { + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.timetableHTML))); + + EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/*") + .withBody(ValuesForTest.listHTML))); + } @Test public void checkParserDataFor12K1_Monday_First() throws IOException { + //given //fetch 12K1 Document document = Jsoup - .connect("https://podzial.mech.pk.edu.pl/stacjonarne/html/plany/o25.html") + .connect(mainUrl + "plany/o25.html") .get(); //Create object - TimetableDTO timeTable = new TimetableDTO("12K1"); + var timeTable = new TimetableDTO("12K1"); - //Call method + //when timeTable.setData(parserService.parse(document.html())); - //Tests + //then assertEquals("12K1", timeTable.getName()); assertEquals("Poniedziałek", timeTable.getData().getFirst().getName()); assertEquals(5, timeTable.getData().size()); @@ -44,16 +67,17 @@ public void checkParserDataFor12K1_Monday_First() throws IOException { @Test public void isHoursListCorrect() throws IOException { - + //given //fetch data Document document = Jsoup - .connect("https://podzial.mech.pk.edu.pl/stacjonarne/html/plany/o25.html") + .connect(mainUrl + "plany/o25.html") .get(); - + //when //call function var result = parserService.parseHours(document.html()); + //then //Check first, last and middle element assertEquals("7:30- 8:15", result.getFirst()); assertEquals("12:45-13:30", result.get(6)); @@ -63,13 +87,17 @@ public void isHoursListCorrect() throws IOException { @Test @WithMockUser public void isGeneralGroupListCorrect() throws IOException { + //given //fetch data Document document = Jsoup - .connect("http://podzial.mech.pk.edu.pl/stacjonarne/html/lista.html") - .get(); + .connect(mainUrl + "lista.html") + .get(); + //when //call method var result = parserService.parseGeneralGroups(document.html()); + + //then //Check if list contains specific elements assertTrue(result.containsKey("12K1")); assertTrue(result.containsKey("11A1")); diff --git a/src/test/java/test/TestConfig.java b/src/test/java/test/TestConfig.java new file mode 100644 index 0000000..aed8624 --- /dev/null +++ b/src/test/java/test/TestConfig.java @@ -0,0 +1,19 @@ +package test; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public abstract class TestConfig { + + protected static final int WIREMOCK_PORT = 9999; + + @RegisterExtension + protected static final WireMockExtension EXTERNAL_SERVICE_API_MOCK = WireMockExtension.newInstance() + .options(wireMockConfig().port(WIREMOCK_PORT)).build(); +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..6224c97 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1 @@ +main.url = http://localhost:9999/ \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..bb5b179 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,13 @@ +#casue issue by some reason +#spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.show-sql=true +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false +spring.jpa.hibernate.ddl-auto=none +spring.datasource.hikari.initialization-fail-timeout=0 + + diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..760aaf7 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,60 @@ +DROP TABLE IF EXISTS exams; +DROP TABLE IF EXISTS exam_type; +DROP TABLE IF EXISTS general_group; +DROP TABLE IF EXISTS groups; +DROP TABLE IF EXISTS otp_codes; +DROP TABLE IF EXISTS users; + +CREATE TABLE exam_type +( + exam_type_id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) +); + +CREATE TABLE general_group +( + general_group_id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) +); + +CREATE TABLE exams +( + exam_id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(255), + description VARCHAR(255), + date TIMESTAMP(6), + "groups" VARCHAR(255), + exam_type_id INT NOT NULL, + FOREIGN KEY (exam_type_id) REFERENCES exam_type (exam_type_id) +); + +CREATE TABLE groups +( + group_id INT PRIMARY KEY AUTO_INCREMENT, + letter CHAR(1) NOT NULL, + group_count INT NOT NULL, + general_group_id INT NOT NULL, + name VARCHAR(255), + FOREIGN KEY (general_group_id) REFERENCES general_group (general_group_id) +); + +CREATE TABLE users +( + user_id INT PRIMARY KEY AUTO_INCREMENT, + general_group_id INT NOT NULL, + email VARCHAR(254) NOT NULL, + is_active BOOLEAN NOT NULL, + role VARCHAR(20) NOT NULL, -- enum zamieniony na VARCHAR + FOREIGN KEY (general_group_id) REFERENCES general_group (general_group_id) +); + +CREATE TABLE otp_codes +( + otp_code_id INT PRIMARY KEY AUTO_INCREMENT, + code VARCHAR(255), + expire TIMESTAMP NOT NULL, + used BOOLEAN NOT NULL, + user_id INT NOT NULL, + timestamp TIMESTAMP(6), + FOREIGN KEY (user_id) REFERENCES users (user_id) +); \ No newline at end of file