diff --git a/README.md b/README.md index 3a2ad12470881fb0a2c87ede48ddca7de7ea52ae..b1befef99ae06ecc28324536ba87af69e813011b 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,37 @@ - Martin GargaloviÄŤ *@xgargal* - Jan PokornĂ˝ *@xpokorn8* - Ester VilĂmková - _Project Leader_ *@xvilimk* + + - **Assigment**: - Create a system for language school lecture management. Each lecture can occur on a given day and time and is related to some course. The course is defined by its (unique) name, language and proficiency level. However, each lecture will be independent. That means that each lecture can have a different topic. Different lecturers can give it, and an arbitrary number of students can enrol. Each lecturer will have a name, surname, and record of taught languages. In addition, the lecturer will indicate that they are a native speaker. Exercises can be prepared for each course to allow the students to learn the language. Each student can pick the exercises depending on the levels of difficulty. -- **Running the modules**: - - ```cd ./application``` - - ```mvn clean install``` - - ```cd ./module-*``` - - ```mvn spring-boot:run``` + +- **Running the modules using docker-compose**: + + ```console + cd ./application + mvn clean install + docker-compose build --parallel + docker-compose up + ``` + +- **Running the modules using podman-compose**: + ~~~console + cd ./application + mvn clean install + podman-compose build --parallel + podman-compose up + ~~~ + +- **Running the modules using docker**: + ~~~console + sudo docker run -d -p 5001:5001 xpokorn8/sprachschulsystem:certificate && + sudo docker run -d -p 5002:5002 xpokorn8/sprachschulsystem:exercise && + sudo docker run -d -p 5000:5000 xpokorn8/sprachschulsystem:language-school && + sudo docker run -d -p 5003:5003 xpokorn8/sprachschulsystem:mail + ~~~ + # Project Description diff --git a/application/docker-compose.yml b/application/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..41a6a0d3edb0fa3ffa8d88abb80419ffb7e96025 --- /dev/null +++ b/application/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + certificate: + build: ./module-certificate + container_name: certificate + image: xpokorn8/sprachschulsystem:certificate + ports: + - "5001:5001" + + exercise: + build: ./module-exercise + container_name: exercise + image: xpokorn8/sprachschulsystem:exercise + ports: + - "5002:5002" + + language-school: + build: ./module-language-school + container_name: language-school + image: xpokorn8/sprachschulsystem:language-school + ports: + - "5000:5000" + + mail: + build: ./module-mail + container_name: mail + image: xpokorn8/sprachschulsystem:mail + ports: + - "5003:5003" \ No newline at end of file diff --git a/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateDto.java b/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateDto.java index 52597af3c50c48aa3adc8136f134aaf05b6f35b7..6d55d5774ba75ebd158a05b9541c943bd2dbc4ac 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateDto.java @@ -33,6 +33,6 @@ public class CertificateDto extends DomainObjectDto { private CertificateFileDto certificateFile; public CertificateDto() { - setId("0"); + setId(0L); } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateSimpleDto.java b/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateSimpleDto.java new file mode 100644 index 0000000000000000000000000000000000000000..52a9f8a59e233654d399ef2f03d69f9c45ac7928 --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/certificate/CertificateSimpleDto.java @@ -0,0 +1,43 @@ +package org.fuseri.model.dto.certificate; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.fuseri.model.dto.common.DomainObjectDto; + +import java.time.Instant; + +/** + * This class represents a Data Transfer Object (DTO) for Certificate entities. + * It is used to transfer Certificate data between different layers of the application. + * It extends the DomainObjectDto class and includes additional Certificate-specific fields. + */ +@Getter +@Setter +public class CertificateSimpleDto extends DomainObjectDto { + @NotBlank + @NotNull + private Long userId; + @NotBlank + @NotNull + private Instant generatedAt; + @NotBlank + @NotNull + private Long courseId; + @NotBlank + @NotNull + private String certificateFile; + @NotBlank + @NotNull + private String certificateFileName; + + public CertificateSimpleDto(Long id, Long userId, Instant generatedAt, Long courseId, String certificateFile, String certificateFileName) { + this.setId(id); + this.userId = userId; + this.generatedAt = generatedAt; + this.courseId = courseId; + this.certificateFile = certificateFile; + this.certificateFileName = certificateFileName; + } +} diff --git a/application/model/src/main/java/org/fuseri/model/dto/common/DomainObjectDto.java b/application/model/src/main/java/org/fuseri/model/dto/common/DomainObjectDto.java index 2c1aff83f7461224ef599deeadb8426945f7e416..0956e176bd13d3cdbdb66ac3b59c36d109a94afe 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/common/DomainObjectDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/common/DomainObjectDto.java @@ -1,15 +1,15 @@ package org.fuseri.model.dto.common; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @Getter @Setter +@EqualsAndHashCode(exclude = "id") public abstract class DomainObjectDto { - @NotBlank @NotNull - private String id; + private Long id; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/common/Result.java b/application/model/src/main/java/org/fuseri/model/dto/common/Result.java deleted file mode 100644 index c75e421e91732dae34d803b7e715f7c395aea240..0000000000000000000000000000000000000000 --- a/application/model/src/main/java/org/fuseri/model/dto/common/Result.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.fuseri.model.dto.common; - -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class Result<T extends DomainObjectDto> { - - private long total; - private int page; - private int pageSize; - private List<T> items; -} diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java index 88995655d12e28abdf75cdd041d93d8d92ff7b8b..2ba3673e96f42b9b3b14fdb6406faed1030268ac 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/CourseCreateDto.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -16,6 +17,7 @@ import lombok.Setter; @Getter @Setter @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) public class CourseCreateDto { @NotBlank(message = "Course name is required") @@ -28,10 +30,10 @@ public class CourseCreateDto { @NotNull(message = "Language type is required") @Valid - private LanguageTypeDto languageTypeDto; + private LanguageTypeDto language; @NotNull(message = "Proficiency level is required") @Valid - private ProficiencyLevelDto proficiencyLevelDto; + private ProficiencyLevelDto proficiency; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java index 544d06f5da178415352ad7c8c826b0bffc3e0fe9..2bb3eb99ccd5b353c16c7813d6170183042dcd62 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/CourseDto.java @@ -9,6 +9,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.fuseri.model.dto.common.DomainObjectDto; +import org.fuseri.model.dto.user.UserDto; import java.util.ArrayList; import java.util.List; @@ -20,7 +21,7 @@ import java.util.List; */ @Getter @Setter -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = false) public class CourseDto extends DomainObjectDto { @NotBlank(message = "Course name is required") @@ -33,22 +34,22 @@ public class CourseDto extends DomainObjectDto { @NotNull(message = "Language type is required") @Valid - private LanguageTypeDto languageTypeDto; + private LanguageTypeDto language; @NotNull(message = "Proficiency level is required") @Valid - private ProficiencyLevelDto proficiencyLevelDto; + private ProficiencyLevelDto proficiency; @NotNull(message = "Student's list is required") @Valid - private List<String> studentIds; + private List<UserDto> students; public CourseDto(String name, Integer capacity, LanguageTypeDto languageTypeDto, ProficiencyLevelDto proficiencyLevelDto) { - setId("0"); this.name = name; this.capacity = capacity; - this.languageTypeDto = languageTypeDto; - this.proficiencyLevelDto = proficiencyLevelDto; - this.studentIds = new ArrayList<>(); + this.language = languageTypeDto; + this.proficiency = proficiencyLevelDto; + this.students = new ArrayList<>(); } + } diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/ProficiencyLevelDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/ProficiencyLevelDto.java index 106973632b9ea136a87bf353b5d51b37f679b6cc..0031b4b936e35cbeb439c1733f02e5bd4bb04f7a 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/course/ProficiencyLevelDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/course/ProficiencyLevelDto.java @@ -1,12 +1,36 @@ package org.fuseri.model.dto.course; +/** + * An enum representing language proficiency levels based on the CEFR standard. + */ public enum ProficiencyLevelDto { - A1, - A2, - B1, - B2, - C1, - C2, - C1N, - C2N -} + + A1("Beginner"), + A2("Elementary"), + B1("Intermediate"), + B2("Upper Intermediate"), + C1("Advanced"), + C2("Proficient"), + C1N("Advanced Native speaker"), + C2N("Proficient Native speaker"); + + private final String description; + + /** + * Constructor for LanguageLevel enum. + * + * @param description a String representing a brief description of the language proficiency level + */ + ProficiencyLevelDto(String description) { + this.description = description; + } + + /** + * Returns a brief description of the language proficiency level. + * + * @return a String representing the description of the language proficiency level + */ + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java index f7a845f6c9bc9970e3e8bcc4d1e2d6e2929c712a..2418847a2d483585f0d0a57505f0cfd148aa2094 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java @@ -15,6 +15,6 @@ public class AnswerCreateDto { @NotNull private boolean correct; - @NotBlank - private String questionId; + @NotNull + private long questionId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java index 433244900c287f01549b3e43a4971f68a86a2cc7..2f87806e2c6869602b12473d464d35b96240aa03 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerDto.java @@ -3,13 +3,13 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.fuseri.model.dto.common.DomainObjectDto; -import java.util.Objects; - @AllArgsConstructor @Getter +@EqualsAndHashCode(callSuper = false) public class AnswerDto extends DomainObjectDto { @NotBlank @@ -17,22 +17,4 @@ public class AnswerDto extends DomainObjectDto { @NotNull private boolean correct; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AnswerDto answerDto = (AnswerDto) o; - - if (correct != answerDto.correct) return false; - return Objects.equals(text, answerDto.text); - } - - @Override - public int hashCode() { - int result = text != null ? text.hashCode() : 0; - result = 31 * result + (correct ? 1 : 0); - return result; - } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java index 04996492928042452c248158a9d4cab9659a772a..f2c7da24058c59d4a600d9a7d7d0403652e46832 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java @@ -1,7 +1,7 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; @@ -11,8 +11,8 @@ import java.util.List; @Getter public class AnswersCreateDto { - @NotBlank - private String questionId; + @NotNull + private long questionId; @Valid private List<AnswerInQuestionCreateDto> answers; diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java index 332b343dce447081c05db0a98e2c57df8815602e..6d1cb1a7727b669e683f775e63aafc0e781816fe 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java @@ -9,7 +9,6 @@ import lombok.Getter; @AllArgsConstructor @Getter public class ExerciseCreateDto { - @NotBlank private String name; @@ -20,6 +19,6 @@ public class ExerciseCreateDto { @PositiveOrZero private int difficulty; - @NotBlank - private String courseId; + @NotNull + private long courseId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java index ff7abd444839ba89d15b50e72f31a3973b62ceef..537d0e63a1da348212904882e3f6d165d6cc9945 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseDto.java @@ -3,14 +3,14 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.fuseri.model.dto.common.DomainObjectDto; -import java.util.Objects; - @Getter @Setter +@EqualsAndHashCode(callSuper = false) public class ExerciseDto extends DomainObjectDto { @NotBlank @@ -23,28 +23,6 @@ public class ExerciseDto extends DomainObjectDto { @PositiveOrZero private int difficulty; - @NotBlank - private String courseId; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ExerciseDto that = (ExerciseDto) o; - - if (difficulty != that.difficulty) return false; - if (!Objects.equals(name, that.name)) return false; - if (!Objects.equals(description, that.description)) return false; - return Objects.equals(courseId, that.courseId); - } - - @Override - public int hashCode() { - int result = name != null ? name.hashCode() : 0; - result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + difficulty; - result = 31 * result + (courseId != null ? courseId.hashCode() : 0); - return result; - } + @NotNull + private long courseId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java index 738a9031739586351ba52b993c7522e0dbcb226d..cd215815305d8bb06de5b81e47f6b6e8380954d2 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java @@ -2,6 +2,7 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,8 +15,8 @@ public class QuestionCreateDto { @NotBlank private String text; - @NotBlank - private String exerciseId; + @NotNull + private long exerciseId; @Valid private List<AnswerInQuestionCreateDto> answers; diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java index 5cca20b14b60c9361eaea0893ff874500b690c61..94ddd64f094b713cb14c216378f181b55844ea51 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionDto.java @@ -2,43 +2,29 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.fuseri.model.dto.common.DomainObjectDto; import java.util.List; -import java.util.Objects; @Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) public class QuestionDto extends DomainObjectDto { @NotBlank private String text; - @NotBlank - private String exerciseId; + @NotNull + private long exerciseId; @Valid private List<AnswerDto> answers; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - QuestionDto that = (QuestionDto) o; - - if (!Objects.equals(text, that.text)) return false; - if (!Objects.equals(exerciseId, that.exerciseId)) return false; - return Objects.equals(answers, that.answers); - } - - @Override - public int hashCode() { - int result = text != null ? text.hashCode() : 0; - result = 31 * result + (exerciseId != null ? exerciseId.hashCode() : 0); - result = 31 * result + (answers != null ? answers.hashCode() : 0); - return result; - } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java index 2efa520493c718e108a180fabae12a4887913cab..59e0fda131a540c0c08da981fd36d156a49cb5ae 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java @@ -1,6 +1,7 @@ package org.fuseri.model.dto.exercise; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; @@ -11,6 +12,6 @@ public class QuestionUpdateDto { @NotBlank private String text; - @NotBlank - private String exerciseId; + @NotNull + private long exerciseId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java index 6fc9b766fefb23e0f31aa2d79af7f30dc9ae7ab2..b7ec0926fd958f722109fcf52421f4c47934f8b4 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureCreateDto.java @@ -3,7 +3,6 @@ package org.fuseri.model.dto.lecture; import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; -import org.fuseri.model.dto.course.CourseDto; import java.time.LocalDateTime; @@ -13,11 +12,11 @@ public class LectureCreateDto { @Future(message = "Lecture start date and time must be in the future") @NotNull(message = "Lecture start date and time cannot be null") - private LocalDateTime from; + private LocalDateTime lectureFrom; @Future(message = "Lecture end date and time must be in the future") @NotNull(message = "Lecture end date and time cannot be null") - private LocalDateTime to; + private LocalDateTime lectureTo; @NotBlank(message = "Lecture topic cannot be blank") @Size(max = 255, message = "Lecture topic must be no more than {max} characters") @@ -27,12 +26,12 @@ public class LectureCreateDto { @Min(value = 1, message = "Lecture capacity must be at least 1") private Integer capacity; - @NotBlank(message = "Lecture course cannot be blank") - private String courseId; + @NotNull(message = "Lecture course cannot be null") + private Long courseId; - public LectureCreateDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, String courseId) { - this.from = from; - this.to = to; + public LectureCreateDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, Long courseId) { + this.lectureFrom = from; + this.lectureTo = to; this.topic = topic; this.capacity = capacity; this.courseId = courseId; @@ -40,7 +39,7 @@ public class LectureCreateDto { @AssertTrue(message = "Lecture start datetime must be before Lecture end datetime") private boolean isFromDateBeforeToDate() { - if (from == null || to == null) return true; - return from.isBefore(to); + if (lectureFrom == null || lectureTo == null) return true; + return lectureFrom.isBefore(lectureTo); } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java index df9b17a7c765223d6e7ae8c56593993298c3fa33..73820adce9c8b5281064b96c6c0811253488b63c 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/lecture/LectureDto.java @@ -1,25 +1,26 @@ package org.fuseri.model.dto.lecture; import jakarta.validation.constraints.*; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.fuseri.model.dto.common.DomainObjectDto; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Getter @Setter +@EqualsAndHashCode(callSuper = false) public class LectureDto extends DomainObjectDto { @Future(message = "Lecture start date and time must be in the future") @NotNull(message = "Lecture start date and time cannot be null") - private LocalDateTime from; + private LocalDateTime lectureFrom; @Future(message = "Lecture end date and time must be in the future") @NotNull(message = "Lecture end date and time cannot be null") - private LocalDateTime to; + private LocalDateTime lectureTo; @NotBlank(message = "Lecture topic cannot be blank") @Size(max = 255, message = "Lecture topic must be no more than {max} characters") @@ -29,28 +30,28 @@ public class LectureDto extends DomainObjectDto { @Min(value = 1, message = "Lecture capacity must be at least 1") private Integer capacity; - @NotNull(message = "Lecture capacity cannot be null") - private String lecturerId; + @NotNull(message = "Lecture lecturer cannot be null") + private Long lecturerId; - @NotBlank(message = "Lecture courseId cannot be blank") - private String courseId; + @NotNull(message = "Lecture courseId cannot be null") + private Long courseId; @NotNull(message = "Student IDs list cannot be null") - private List<String> studentIds; + private List<Long> students; - public LectureDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, String lecturerId, String courseId) { - this.from = from; - this.to = to; + public LectureDto(LocalDateTime from, LocalDateTime to, String topic, Integer capacity, Long lecturerId, Long courseId, List<Long> students) { + this.lectureFrom = from; + this.lectureTo = to; this.topic = topic; this.capacity = capacity; this.lecturerId = lecturerId; this.courseId = courseId; - this.studentIds = new ArrayList<>(); + this.students = students; } @AssertTrue(message = "Lecture start datetime must be before Lecture end datetime") private boolean isFromDateBeforeToDate() { - if (from == null || to == null) return true; - return from.isBefore(to); + if (lectureFrom == null || lectureTo == null) return true; + return lectureFrom.isBefore(lectureTo); } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java index bcbbbe4a3a2280476f47e72ab406052843a67b50..8cc6071b122bf90572c3134039b163842ee7751b 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java @@ -2,6 +2,7 @@ package org.fuseri.model.dto.user; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -10,15 +11,21 @@ import lombok.Setter; @Setter @AllArgsConstructor @NoArgsConstructor +@EqualsAndHashCode public class AddressDto { + @NotBlank private String country; + @NotBlank private String city; + @NotBlank private String street; + @NotBlank private String houseNumber; + @NotBlank private String zip; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java index 9be887e9319946ecf9e73f1188e7c94201a1d874..2336e937e1d65e819bd2095e9641213942d0fc8e 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java @@ -4,25 +4,43 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; + +import java.util.Map; @Getter @Setter @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) public class UserCreateDto { @NotBlank private String username; + @NotBlank private String password; + @NotBlank private String email; + @NotBlank private String firstName; + @NotBlank private String lastName; + @NotNull @Valid private AddressDto address; + + @NotNull + private UserType userType; + + @NotNull + @Valid + private Map<LanguageTypeDto, ProficiencyLevelDto> languageProficiency; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserDto.java index fce12aabfb8baadbeb620c531ffcc84a92c4bb77..0e3ebf029e28af0b99d1d9c47fadfc2bf776c780 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/user/UserDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserDto.java @@ -1,31 +1,50 @@ package org.fuseri.model.dto.user; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; import org.fuseri.model.dto.common.DomainObjectDto; import lombok.Getter; import lombok.Setter; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; + +import java.util.Map; @Getter @Setter +@EqualsAndHashCode(callSuper = false) public class UserDto extends DomainObjectDto { + @NotBlank private String username; + @NotBlank private String email; + @NotBlank private String firstName; + @NotBlank private String lastName; + + @Valid private AddressDto address; + @NotNull + private UserType userType; + + @NotNull + @Valid + private Map<LanguageTypeDto, ProficiencyLevelDto> languageProficiency; - public UserDto(String username, String email, String firstName, String lastName, AddressDto address) { - setId("0"); + public UserDto(String username, String email, String firstName, String lastName, AddressDto address, UserType userType) { this.username = username; this.email = email; this.firstName = firstName; this.lastName = lastName; this.address = address; + this.userType = userType; } } diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java index 23972ce45017b07c3507535167952d16dc8ecd15..45ed8e257d736edd26a79f6d07fb1137455676d7 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java @@ -9,8 +9,10 @@ import lombok.Setter; @Setter @AllArgsConstructor public class UserLoginDto { + @NotBlank private String username; + @NotBlank private String password; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserType.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserType.java new file mode 100644 index 0000000000000000000000000000000000000000..1c38db11b25b25eee6c947f37c388724ea8ffdb9 --- /dev/null +++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserType.java @@ -0,0 +1,7 @@ +package org.fuseri.model.dto.user; + +public enum UserType { + ADMIN, + STUDENT, + LECTURER +} diff --git a/application/module-certificate/Dockerfile b/application/module-certificate/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..21e346d921cf9c19dd93c4fc3c5d29555c2aa59a --- /dev/null +++ b/application/module-certificate/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-certificate-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/module-certificate/pom.xml b/application/module-certificate/pom.xml index 2513ce45c90524fd4f223700e24447c54e867d36..9ab80d528d4f1ca8c3487a1df99a9cdf450cba7d 100644 --- a/application/module-certificate/pom.xml +++ b/application/module-certificate/pom.xml @@ -61,6 +61,18 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-tx</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + </dependency> </dependencies> <build> diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/Certificate.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/Certificate.java new file mode 100644 index 0000000000000000000000000000000000000000..3b9b0a94666ab63b462e85c64e177f467769cb20 --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/Certificate.java @@ -0,0 +1,104 @@ +package org.fuseri.modulecertificate; + +import jakarta.persistence.*; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; + +@Entity +@Table(name = "certificate") +public class Certificate implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id_certificate") + private Long id; + @Column(name = "id_user") + private Long userId; + @Column(name = "generatedAt") + @Temporal(TemporalType.TIMESTAMP) + private Instant generatedAt; + @Column(name = "id_course") + private Long courseId; + @Column(name = "certificate_file") + private String certificateFile; + @Column(name = "certificate_file_name") + private String certificateFileName; + + public Certificate() { + } + + public Certificate(Long userId, Instant generatedAt, Long courseId, String certificateFile, String certificateFileName) { + this.userId = userId; + this.generatedAt = generatedAt; + this.courseId = courseId; + this.certificateFile = certificateFile; + this.certificateFileName = certificateFileName; + } + + public Long getId() { + return id; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Instant getGeneratedAt() { + return generatedAt; + } + + public void setGeneratedAt(Instant generatedAt) { + this.generatedAt = generatedAt; + } + + public Long getCourseId() { + return courseId; + } + + public void setCourseId(Long courseId) { + this.courseId = courseId; + } + + public String getCertificateFile() { + return certificateFile; + } + + public void setCertificateFile(String certificateFile) { + this.certificateFile = certificateFile; + } + + public String getCertificateFileName() { + return certificateFileName; + } + + public void setCertificateFileName(String certificateFileName) { + this.certificateFileName = certificateFileName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Certificate certificate)) { + return false; + } + return Objects.equals(getUserId(), certificate.getUserId()) + && Objects.equals(getGeneratedAt(), certificate.getGeneratedAt()) + && Objects.equals(getCourseId(), certificate.getCourseId()) + && Objects.equals(getCertificateFile(), certificate.getCertificateFile()) + && Objects.equals(getCertificateFileName(), certificate.getCertificateFileName()); + } + + @Override + public int hashCode() { + return Objects.hash(getUserId(), getGeneratedAt(), getCourseId(), getCertificateFile(), getCertificateFileName()); + } +} + diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java index bfb6447306aeb78f47b1dd3878e114c955c5c8e9..e63254df618398a0faf40d7360551918cf9e5cdb 100644 --- a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateController.java @@ -1,11 +1,19 @@ package org.fuseri.modulecertificate.service; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.certificate.CertificateCreateDto; -import org.fuseri.model.dto.certificate.CertificateDto; -import org.fuseri.model.dto.common.Result; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpStatus; import java.util.List; @@ -18,19 +26,30 @@ import java.util.List; @RequestMapping("/certificates") public class CertificateController { + private final CertificateFacade certificateFacade; + @Autowired - public CertificateController() { + public CertificateController(CertificateFacade certificateFacade) { + this.certificateFacade = certificateFacade; } + /** * Generates certificate for specified user and course. * * @param certificateCreateDto Dto with data used for generating certificate * @return CertificateDto with data of generated certificate */ - @PostMapping("/generate") - public CertificateDto generate(@Valid @RequestBody CertificateCreateDto certificateCreateDto) { - return new CertificateDto(); + @Operation(summary = "Generate certificate", + description = "Generates certificate, saves it into database and returns certificate information and certificate file.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Certificate generated successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input.") + }) + @PostMapping + public ResponseEntity<CertificateSimpleDto> generate(@Valid @RequestBody CertificateCreateDto certificateCreateDto) { + var certificateSimpleDto = certificateFacade.generate(certificateCreateDto); + return ResponseEntity.status(HttpStatus.CREATED).body(certificateSimpleDto); } /** @@ -39,9 +58,18 @@ public class CertificateController { * @param id ID of certificate to be retrieved * @return CertificateDto with data of previously generated certificate with specified ID */ - @GetMapping("/find") - public CertificateDto find(@RequestParam String id) { - return new CertificateDto(); + @Operation(summary = "Get a certificate by ID", description = "Returns a certificate with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Certificate with the specified ID retrieved successfully."), + @ApiResponse(responseCode = "404", description = "Certificate with the specified ID was not found.") + }) + @GetMapping("/{id}") + public ResponseEntity<CertificateSimpleDto> find(@NotNull @PathVariable Long id) { + try { + return ResponseEntity.ok(certificateFacade.findById(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } /** @@ -51,9 +79,14 @@ public class CertificateController { * @return List of CertificateDto objects with previously generated certificates * for specified User. */ + @Operation(summary = "Get certificates for user", description = "Returns certificates for given user in list.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved certificates"), + @ApiResponse(responseCode = "400", description = "Invalid input."), + }) @GetMapping("/findForUser") - public List<CertificateDto> findForUser(@RequestParam String userId) { - return List.of(new CertificateDto()); + public ResponseEntity<List<CertificateSimpleDto>> findForUser(@RequestParam Long userId) { + return ResponseEntity.ok(certificateFacade.findByUserId(userId)); } /** @@ -62,11 +95,17 @@ public class CertificateController { * @param userId ID of user to retrieve certificates for. * @param courseId ID of course to retrieve certificates for. * @return List of CertificateDto objects with previously generated certificates - * for specified User. + * for specified User and Course. */ - @GetMapping("/getId") - public String getId(@RequestParam String userId, @RequestParam String courseId) { - return "0"; + @Operation(summary = "Get certificates for user and course", + description = "Returns certificates for given user and course in list.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved certificates"), + @ApiResponse(responseCode = "400", description = "Invalid input."), + }) + @GetMapping("/findForUserAndCourse") + public ResponseEntity<List<CertificateSimpleDto>> findForUserAndCourse(@RequestParam Long userId, @RequestParam Long courseId) { + return ResponseEntity.ok(certificateFacade.findByUserIdAndCourseId(userId, courseId)); } /** @@ -74,18 +113,28 @@ public class CertificateController { * * @param id Id of certificate to be deleted. */ - @DeleteMapping("/delete") - public void delete(@RequestParam String id) { + @Operation(summary = "Delete a certificate with specified ID", description = "Deletes a certificate with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Certificate with the specified ID deleted successfully."), + }) + @DeleteMapping("/{id}") + public ResponseEntity<Void> delete(@NotNull @PathVariable Long id) { + certificateFacade.deleteCertificate(id); + return ResponseEntity.noContent().build(); } /** * Find certificates and return them in a paginated format * - * @param page the page number of the certificates to retrieve * @return a Result object containing a list of CertificateDto objects and pagination information */ - @GetMapping("/findAll") - public Result<CertificateDto> findAllCertificates(@RequestParam int page) { - return new Result<>(); + @Operation(summary = "Get certificates in paginated format", description = "Returns certificates in paginated format.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved paginated certificates"), + @ApiResponse(responseCode = "400", description = "Invalid page number supplied"), + }) + @GetMapping + public ResponseEntity<Page<CertificateSimpleDto>> findAllCertificates(Pageable pageable) { + return ResponseEntity.ok(certificateFacade.findAll(pageable)); } } diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateFacade.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..018c0591e6e30f92730d5cf7374fa26defca3b23 --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateFacade.java @@ -0,0 +1,60 @@ +package org.fuseri.modulecertificate.service; + + +import org.fuseri.model.dto.certificate.CertificateCreateDto; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class CertificateFacade { + + private final CertificateService certificateService; + private final CertificateMapper certificateMapper; + + @Autowired + public CertificateFacade(CertificateService certificateService, CertificateMapper certificateMapper) { + this.certificateService = certificateService; + this.certificateMapper = certificateMapper; + } + + @Cacheable(cacheNames = "certificates", key = "#id") + @Transactional(readOnly = true) + public CertificateSimpleDto findById(Long id) { + return certificateMapper.mapToSimpleDto(certificateService.findById(id)); + } + + @Transactional() + public CertificateSimpleDto generate(CertificateCreateDto certificateCreateDto) { + return certificateMapper.mapToSimpleDto(certificateService.save(certificateMapper.mapToCertificate(certificateCreateDto))); + } + + @Cacheable(cacheNames = "certificates", key = "#userId") + @Transactional(readOnly = true) + public List<CertificateSimpleDto> findByUserId(Long userId) { + return certificateMapper.mapToList(certificateService.findByUserId(userId)); + } + + @Cacheable(cacheNames = "certificates", key = "#userId") + @Transactional(readOnly = true) + public List<CertificateSimpleDto> findByUserIdAndCourseId(Long userId, Long courseId) { + return certificateMapper.mapToList(certificateService.findByUserIdAndCourseId(userId, courseId)); + } + + @Transactional(readOnly = true) + public Page<CertificateSimpleDto> findAll(Pageable pageable) { + return certificateMapper.mapToPageDto(certificateService.findAll(pageable)); + } + + @Transactional(readOnly = true) + public void deleteCertificate(Long certificateId) { + certificateService.delete(certificateId); + } +} diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateMapper.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..f6738c4428f97b4bb6cacf0b2806491aabf00e8b --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateMapper.java @@ -0,0 +1,50 @@ +package org.fuseri.modulecertificate.service; + +import org.fuseri.model.dto.certificate.CertificateCreateDto; +import org.fuseri.model.dto.certificate.CertificateDto; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; +import org.fuseri.modulecertificate.Certificate; +import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.time.Instant; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CertificateMapper { + + default CertificateSimpleDto mapToSimpleDto(Certificate certificate) + { + if ( certificate == null ) { + return null; + } + + return new CertificateSimpleDto( + certificate.getId(), + certificate.getUserId(), + certificate.getGeneratedAt(), + certificate.getCourseId(), + certificate.getCertificateFile(), + certificate.getCertificateFileName()); + } + + default Certificate mapToCertificate(CertificateCreateDto certificateCreateDto) + { + if ( certificateCreateDto == null ) { + return null; + } + + return new Certificate(certificateCreateDto.getUser().getId(), + Instant.now(), + certificateCreateDto.getCourse().getId(), + null, + null); + } + + List<CertificateSimpleDto> mapToList(List<Certificate> certificates); + + default Page<CertificateSimpleDto> mapToPageDto(Page<Certificate> certificates) { + return new PageImpl<>(mapToList(certificates.getContent()), certificates.getPageable(), certificates.getTotalPages()); + } +} diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateRepository.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..832d2d22d002b82e3a93287f4382cfa66273b56d --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateRepository.java @@ -0,0 +1,21 @@ +package org.fuseri.modulecertificate.service; + +import org.fuseri.modulecertificate.Certificate; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Repository +public interface CertificateRepository extends JpaRepository<Certificate, Long> { + + @Query("SELECT c FROM Certificate c WHERE c.userId = ?1") + List<Certificate> findCertificateByUserId(Long userId); + + @Query("SELECT c FROM Certificate c WHERE c.userId = ?1 and c.courseId = ?2") + List<Certificate> findCertificateByUserIdAndCourseId(Long userId, Long courseId); + +} + diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateService.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateService.java new file mode 100644 index 0000000000000000000000000000000000000000..245c88c35ea55f4d59d0d01ed2b15290fddc42ed --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/CertificateService.java @@ -0,0 +1,55 @@ +package org.fuseri.modulecertificate.service; + +import org.fuseri.modulecertificate.Certificate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +public class CertificateService { + + private final CertificateRepository certificateRepository; + + @Autowired + public CertificateService(CertificateRepository certificateRepository) { + this.certificateRepository = certificateRepository; + } + + @Transactional(readOnly = true) + public Certificate findById(Long id) { + return certificateRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Certificate with id: " + id + " was not found.")); + } + + @Transactional(readOnly = true) + public List<Certificate> findByUserId(Long userId) { + return certificateRepository.findCertificateByUserId(userId); + } + + @Transactional(readOnly = true) + public List<Certificate> findByUserIdAndCourseId(Long userId, Long courseId) { + return certificateRepository.findCertificateByUserIdAndCourseId(userId, courseId); + } + + @Transactional(readOnly = true) + public Certificate save(Certificate certificate) { + return certificateRepository.save(certificate); + } + + @Transactional(readOnly = true) + public void delete(Long certificateId) { + certificateRepository.deleteById(certificateId); + } + + @Transactional(readOnly = true) + public Page<Certificate> findAll(Pageable pageable) { + return certificateRepository.findAll(pageable); + } +} + diff --git a/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/ResourceNotFoundException.java b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..d24c445da1fcc4ba826a0e614778823400a4c719 --- /dev/null +++ b/application/module-certificate/src/main/java/org/fuseri/modulecertificate/service/ResourceNotFoundException.java @@ -0,0 +1,22 @@ +package org.fuseri.modulecertificate.service; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException() { + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/application/module-certificate/src/main/resources/application.properties b/application/module-certificate/src/main/resources/application.properties index 17abda7ad394e3595b5f09d64de61a54cf12235e..21fb3ce44a10a9d8eb2c663388416536d4ddaf53 100644 --- a/application/module-certificate/src/main/resources/application.properties +++ b/application/module-certificate/src/main/resources/application.properties @@ -1 +1,10 @@ -server.port=5001 \ No newline at end of file +server.port=5001 + +spring.jpa.open-in-view=false +spring.datasource.url=jdbc:h2:mem:social-network;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=SedaQ-app +spring.datasource.password=$argon2id$v=19$m=16,t=2,p=1$YmF0bWFuYmF0bWFu$MdHYB359HdivAb9J6CaILw +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +# showing SQL is generally good practice for running project locally to check whether there is not an issue with implementation of JPA methods. +spring.jpa.show-sql=true \ No newline at end of file diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java index 08ba4e44f5b195272667539b3a663c28baf003fb..a74e9f10e84d11bdd60549886981f91d9d480025 100644 --- a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateControllerTests.java @@ -2,19 +2,30 @@ package org.fuseri.modulecertificate; import com.fasterxml.jackson.databind.ObjectMapper; import org.fuseri.model.dto.certificate.CertificateCreateDto; -import org.fuseri.model.dto.certificate.CertificateDto; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.course.LanguageTypeDto; import org.fuseri.model.dto.course.ProficiencyLevelDto; import org.fuseri.model.dto.user.AddressDto; import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulecertificate.service.CertificateFacade; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; 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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.time.Instant; +import java.util.List; + import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -23,23 +34,26 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @AutoConfigureMockMvc class CertificateControllerTests { - @Autowired - private ObjectMapper objectMapper; + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final CourseDto COURSE = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final CertificateCreateDto certificateCreateDto = new CertificateCreateDto(USER, COURSE); + private final CertificateSimpleDto certificateDto = new CertificateSimpleDto(0L, USER.getId(), + Instant.now(), COURSE.getId(), "", ""); @Autowired private MockMvc mockMvc; - private final UserDto USER = new UserDto("novakovat", - "novakova@gamil.com", "Tereza", "Nováková", new AddressDto()); - private final CourseDto COURSE = new CourseDto("AJ1", 10, - LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + @MockBean + private CertificateFacade certificateFacade; - @Test - void generateCertificate() throws Exception { - mockMvc.perform(post("/certificates/generate") - .content(asJsonString(new CertificateCreateDto(USER, COURSE))) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + private static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Test @@ -67,26 +81,23 @@ class CertificateControllerTests { @Test void findCertificate() throws Exception { - String response = mockMvc.perform(post("/certificates/generate") - .content(asJsonString(new CertificateCreateDto(USER, COURSE))) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + Mockito.when(certificateFacade.findById(ArgumentMatchers.anyLong())).thenReturn(certificateDto); - String id = objectMapper.readValue(response, CertificateDto.class).getId(); - - mockMvc.perform(get("/certificates/find").param("id", id)) + mockMvc.perform(get("/certificates/" + certificateDto.getId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(id)); + .andExpect(jsonPath("$.id").value(certificateDto.getId())); } @Test void findCertificateWithoutId() throws Exception { - mockMvc.perform(get("/certificates/find")) + mockMvc.perform(get("/certificates/")) .andExpect(status().is4xxClientError()); } @Test void findCertificatesForUser() throws Exception { + Mockito.when(certificateFacade.findByUserId(ArgumentMatchers.anyLong())).thenReturn(List.of(certificateDto)); + mockMvc.perform(get("/certificates/findForUser").param("userId", "0")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) @@ -101,65 +112,73 @@ class CertificateControllerTests { @Test void findCertificateIdForUserAndCourse() throws Exception { - mockMvc.perform(get("/certificates/getId") + Mockito.when(certificateFacade.findByUserIdAndCourseId(ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong())) + .thenReturn(List.of(certificateDto)); + + mockMvc.perform(get("/certificates/findForUserAndCourse") .param("userId", "0") .param("courseId", "0")) .andExpect(status().isOk()) - .andExpect(content().string("0")); + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isNotEmpty()); } @Test void findCertificateIdWithoutUserId() throws Exception { - mockMvc.perform(get("/certificates/getId") + mockMvc.perform(get("/certificates/findForUserAndCourse") .param("courseId", "0")) .andExpect(status().is4xxClientError()); } @Test void findCertificateIdWithoutCourseId() throws Exception { - mockMvc.perform(get("/certificates/getId") + mockMvc.perform(get("/certificates/findForUserAndCourse") .param("userId", "0")) .andExpect(status().is4xxClientError()); } @Test void findCertificateIdWithoutParams() throws Exception { - mockMvc.perform(get("/certificates/getId")) + mockMvc.perform(get("/certificates/findForUserAndCourse")) .andExpect(status().is4xxClientError()); } @Test void deleteCertificate() throws Exception { - mockMvc.perform(delete("/certificates/delete") - .param("id", "0")) - .andExpect(status().isOk()); + Mockito.doNothing().when(certificateFacade).deleteCertificate(ArgumentMatchers.anyLong()); + + mockMvc.perform(delete("/certificates/" + 0L)) + .andExpect(status().is2xxSuccessful()); } @Test void deleteCertificateWithoutParam() throws Exception { - mockMvc.perform(delete("/certificates/delete")) + mockMvc.perform(delete("/certificates/")) .andExpect(status().is4xxClientError()); } - @Test void findAllCertificates() throws Exception { - mockMvc.perform(get("/certificates/findAll") - .param("page", "1")) - .andExpect(status().isOk()); + Mockito.when(certificateFacade.findAll(ArgumentMatchers.any(Pageable.class))) + .thenReturn(Page.empty(PageRequest.of(0, 1))); + + mockMvc.perform(get("/certificates") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.content").isEmpty()); } @Test void findAllCertificatesWithoutParam() throws Exception { - mockMvc.perform(get("/certificates/findAll")) - .andExpect(status().is4xxClientError()); - } + Mockito.when(certificateFacade.findAll(ArgumentMatchers.any(Pageable.class))) + .thenReturn(Page.empty(PageRequest.of(0, 1))); - private static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } + mockMvc.perform(get("/certificates")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.content").isEmpty()); } } diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateFacadeTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateFacadeTests.java new file mode 100644 index 0000000000000000000000000000000000000000..6875ef6247bd33155f98d8bf6c651cabc2fd0f04 --- /dev/null +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateFacadeTests.java @@ -0,0 +1,122 @@ +package org.fuseri.modulecertificate; + +import org.fuseri.model.dto.certificate.CertificateCreateDto; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulecertificate.service.CertificateFacade; +import org.fuseri.modulecertificate.service.CertificateMapper; +import org.fuseri.modulecertificate.service.CertificateService; +import org.junit.jupiter.api.Test; +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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +@AutoConfigureMockMvc +final class CertificateFacadeTests { + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final CourseDto COURSE = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final CertificateCreateDto certificateCreateDto = new CertificateCreateDto(USER, COURSE); + private final CertificateSimpleDto certificateDto = new CertificateSimpleDto(0L, USER.getId(), + Instant.now(), COURSE.getId(), "", ""); + + private final Certificate certificate = new Certificate(); + private final List<Certificate> certificateList = List.of(certificate); + + @Autowired + private CertificateFacade certificateFacade; + + @MockBean + private CertificateService certificateService; + + @MockBean + private CertificateMapper certificateMapper; + + @Test + void generateCertificate() { + when(certificateMapper.mapToCertificate(certificateCreateDto)).thenReturn(certificate); + when(certificateService.save(certificate)).thenReturn(certificate); + when(certificateMapper.mapToSimpleDto(certificate)).thenReturn(certificateDto); + + CertificateSimpleDto actualDto = certificateFacade.generate(certificateCreateDto); + + assertEquals(certificateDto, actualDto); + } + + @Test + public void testFindById() { + Long id = 0L; + when(certificateService.findById(id)).thenReturn(certificate); + when(certificateMapper.mapToSimpleDto(certificate)).thenReturn(certificateDto); + + CertificateSimpleDto actualDto = certificateFacade.findById(id); + + assertNotNull(actualDto); + assertEquals(certificateDto, actualDto); + } + + @Test + public void testFindForUser() { + List<CertificateSimpleDto> expectedDtos = List.of(certificateDto); + + when(certificateService.findByUserId(USER.getId())).thenReturn(certificateList); + when(certificateMapper.mapToList(certificateList)).thenReturn(expectedDtos); + + List<CertificateSimpleDto> actualDtos = certificateFacade.findByUserId(USER.getId()); + + assertEquals(expectedDtos, actualDtos); + } + + @Test + public void testFindForUserAndCourse() { + List<CertificateSimpleDto> expectedDtos = List.of(certificateDto); + + when(certificateService.findByUserIdAndCourseId(USER.getId(), COURSE.getId())).thenReturn(certificateList); + when(certificateMapper.mapToList(certificateList)).thenReturn(expectedDtos); + + List<CertificateSimpleDto> actualDtos = certificateFacade.findByUserIdAndCourseId(USER.getId(), COURSE.getId()); + + assertEquals(expectedDtos, actualDtos); + } + + @Test + public void testDelete() { + Long certificateId = 1L; + certificateFacade.deleteCertificate(certificateId); + verify(certificateService).delete(certificateId); + } + + @Test + public void testFindAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<Certificate> certificatePage = new PageImpl<>(List.of(certificate), pageable, 0); + Page<CertificateSimpleDto> expectedPageDto = new PageImpl<>(List.of(certificateDto), pageable, 0); + + when(certificateService.findAll(pageable)).thenReturn(certificatePage); + when(certificateMapper.mapToPageDto(certificatePage)).thenReturn(expectedPageDto); + + Page<CertificateSimpleDto> actualPageDto = certificateFacade.findAll(pageable); + + assertEquals(expectedPageDto, actualPageDto); + } +} diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateMapperTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateMapperTests.java new file mode 100644 index 0000000000000000000000000000000000000000..00cf08aa380c2a3d068315df2bbd5119c7acb08d --- /dev/null +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateMapperTests.java @@ -0,0 +1,133 @@ +package org.fuseri.modulecertificate; + +import org.fuseri.model.dto.certificate.CertificateCreateDto; +import org.fuseri.model.dto.certificate.CertificateSimpleDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulecertificate.service.CertificateMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +@SpringBootTest +final class CertificateMapperTests { + + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final CourseDto COURSE = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final Instant instant = Instant.now(); + private final String fileName = "fileName"; + private final String file = "file"; + private final CertificateSimpleDto certificateDto = new CertificateSimpleDto(0L, USER.getId(), + instant, COURSE.getId(), file, fileName); + private final Certificate certificate = new Certificate(USER.getId(), + instant, COURSE.getId(), file, fileName); + private final CertificateCreateDto certificateCreateDto = new CertificateCreateDto(USER, COURSE); + @Autowired + private CertificateMapper certificateMapper; + + @Test + void mapNullToSimpleDto() { + var createdSimpleDto = certificateMapper.mapToSimpleDto(null); + + Assertions.assertNull(createdSimpleDto); + } + + @Test + void mapToSimpleDto() { + var createdSimpleDto = certificateMapper.mapToSimpleDto(certificate); + + Assertions.assertEquals(createdSimpleDto.getUserId(), certificateDto.getUserId()); + Assertions.assertEquals(createdSimpleDto.getGeneratedAt(), certificateDto.getGeneratedAt()); + Assertions.assertEquals(createdSimpleDto.getCourseId(), certificateDto.getCourseId()); + Assertions.assertEquals(createdSimpleDto.getCertificateFile(), certificateDto.getCertificateFile()); + Assertions.assertEquals(createdSimpleDto.getCertificateFileName(), certificateDto.getCertificateFileName()); + } + + @Test + void mapNullToCertificate() { + var createdCertificate = certificateMapper.mapToCertificate(null); + + Assertions.assertNull(createdCertificate); + } + + @Test + void mapToCertificate() { + var createdCertificate = certificateMapper.mapToCertificate(certificateCreateDto); + + Assertions.assertEquals(createdCertificate.getUserId(), certificateDto.getUserId()); + Assertions.assertNotNull(createdCertificate.getGeneratedAt()); + Assertions.assertEquals(createdCertificate.getCourseId(), certificateDto.getCourseId()); + Assertions.assertNull(createdCertificate.getCertificateFile()); + Assertions.assertNull(createdCertificate.getCertificateFileName()); + } + + @Test + void mapNullToList() { + var certificateSimpleDtos = certificateMapper.mapToList(null); + + Assertions.assertNull(certificateSimpleDtos); + } + + @Test + void mapToEmptyList() { + var certificateSimpleDtos = certificateMapper.mapToList(Collections.emptyList()); + + Assertions.assertEquals(certificateSimpleDtos.size(), 0); + } + + @Test + void mapToList() { + var certificateSimpleDtos = certificateMapper.mapToList(Collections.singletonList(certificate)); + + Assertions.assertEquals(certificateSimpleDtos.size(), 1); + Assertions.assertEquals(certificateSimpleDtos.get(0).getUserId(), certificateDto.getUserId()); + Assertions.assertEquals(certificateSimpleDtos.get(0).getGeneratedAt(), certificateDto.getGeneratedAt()); + Assertions.assertEquals(certificateSimpleDtos.get(0).getCourseId(), certificateDto.getCourseId()); + Assertions.assertEquals(certificateSimpleDtos.get(0).getCertificateFile(), certificateDto.getCertificateFile()); + Assertions.assertEquals(certificateSimpleDtos.get(0).getCertificateFileName(), certificateDto.getCertificateFileName()); + + + } + + @Test + void mapToEmptyPageDto() { + Page<CertificateSimpleDto> pageDto = certificateMapper.mapToPageDto(Page.empty()); + + Assertions.assertEquals(1, pageDto.getTotalPages()); + } + + @Test + void mapToPageDto() { + List<Certificate> certificates = List.of(certificate); + Page<Certificate> page = new PageImpl<>(certificates, PageRequest.of(0, 1), certificates.size()); + Page<CertificateSimpleDto> pageDto = certificateMapper.mapToPageDto(page); + + Assertions.assertEquals(page.getTotalPages(), pageDto.getTotalPages()); + Assertions.assertEquals(page.getNumber(), pageDto.getNumber()); + Assertions.assertEquals(page.getNumberOfElements(), pageDto.getNumberOfElements()); + Assertions.assertEquals(page.getSize(), pageDto.getSize()); + Assertions.assertEquals(page.getTotalElements(), pageDto.getTotalElements()); + + Assertions.assertEquals(certificate.getId(), pageDto.getContent().get(0).getId()); + Assertions.assertEquals(certificate.getGeneratedAt(), pageDto.getContent().get(0).getGeneratedAt()); + Assertions.assertEquals(certificate.getUserId(), pageDto.getContent().get(0).getUserId()); + Assertions.assertEquals(certificate.getCourseId(), pageDto.getContent().get(0).getCourseId()); + Assertions.assertEquals(certificate.getCertificateFile(), pageDto.getContent().get(0).getCertificateFile()); + Assertions.assertEquals(certificate.getCertificateFileName(), pageDto.getContent().get(0).getCertificateFileName()); + } + +} diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateRepositoryTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateRepositoryTests.java new file mode 100644 index 0000000000000000000000000000000000000000..34ca57badbed1ae6a79c31b20ef4f810e40fc14c --- /dev/null +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateRepositoryTests.java @@ -0,0 +1,101 @@ +package org.fuseri.modulecertificate; + +import org.fuseri.modulecertificate.service.CertificateRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.Arrays; +import java.util.List; + +@DataJpaTest +class CertificateRepositoryTests { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private CertificateRepository certificateRepository; + + @Test + void saveCertificate() { + Certificate certificate = new Certificate(); + certificate.setUserId(1L); + certificate.setCourseId(2L); + + Certificate saved = certificateRepository.save(certificate); + + Assertions.assertNotNull(saved); + Assertions.assertEquals(certificate, saved); + } + + @Test + void findCertificate() { + Certificate certificate = new Certificate(); + certificate.setUserId(1L); + entityManager.persist(certificate); + entityManager.flush(); + + Certificate found = certificateRepository.findById(certificate.getId()).orElse(null); + + Assertions.assertNotNull(found); + Assertions.assertEquals(found, certificate); + } + + @Test + void findCertificateByUserId() { + Certificate certificate = new Certificate(); + certificate.setUserId(1L); + entityManager.persist(certificate); + entityManager.flush(); + + List<Certificate> found = certificateRepository.findCertificateByUserId(1L); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), certificate); + } + + @Test + void findCertificateByUserIdAndCourseId() { + Certificate certificate = new Certificate(); + certificate.setUserId(1L); + certificate.setCourseId(2L); + entityManager.persist(certificate); + entityManager.flush(); + + List<Certificate> found = certificateRepository.findCertificateByUserIdAndCourseId(1L, 2L); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), certificate); + } + + @Test + public void testFindAllCertificates() { + Certificate certificate1 = new Certificate(); + Certificate certificate2 = new Certificate(); + + certificateRepository.save(certificate1); + certificateRepository.save(certificate2); + + Page<Certificate> certificatePage = certificateRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, certificatePage.getTotalElements()); + Assertions.assertEquals(certificatePage.getContent(), Arrays.asList(certificate1, certificate2)); + } + + @Test + public void testDeleteCertificate() { + Long certificateId = entityManager.persist(new Certificate()).getId(); + entityManager.flush(); + + certificateRepository.deleteById(certificateId); + + Assertions.assertTrue(certificateRepository.findById(certificateId).isEmpty()); + } + + +} diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateServiceTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateServiceTests.java new file mode 100644 index 0000000000000000000000000000000000000000..dc11b78a1734722c0926a440e16329a8853bfeff --- /dev/null +++ b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/CertificateServiceTests.java @@ -0,0 +1,118 @@ +package org.fuseri.modulecertificate; + +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulecertificate.service.CertificateRepository; +import org.fuseri.modulecertificate.service.CertificateService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.mockito.Mockito.*; + +@SpringBootTest +final class CertificateServiceTests { + + @MockBean + private CertificateRepository certificateRepository; + + @Autowired + private CertificateService certificateService; + + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final CourseDto COURSE = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final Certificate certificate = new Certificate(USER.getId(), + Instant.now(), COURSE.getId(), "file", "fileName"); + private final List<Certificate> certificates = List.of(certificate, certificate); + + @Test + void save() { + when(certificateRepository.save(certificate)).thenReturn(certificate); + + Certificate result = certificateService.save(certificate); + + Assertions.assertEquals(certificate, result); + verify(certificateRepository).save(certificate); + } + + @Test + void notFoundById() { + when(certificateRepository.findById(certificate.getId())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> certificateService.findById(certificate.getId())); + + } + + @Test + void findById() { + when(certificateRepository.findById(certificate.getId())).thenReturn(Optional.of(certificate)); + + Certificate result = certificateService.findById(certificate.getId()); + + Assertions.assertEquals(certificate, result); + verify(certificateRepository).findById(certificate.getId()); + } + + + + @Test + void findByUserId() { + when(certificateRepository.findCertificateByUserId(USER.getId())).thenReturn(certificates); + + List<Certificate> result = certificateService.findByUserId(USER.getId()); + + Assertions.assertEquals(certificates, result); + verify(certificateRepository).findCertificateByUserId(USER.getId()); + } + + @Test + void findByUserIdAndCourseId() { + when(certificateRepository.findCertificateByUserIdAndCourseId(USER.getId(), COURSE.getId())).thenReturn(certificates); + + List<Certificate> result = certificateService.findByUserIdAndCourseId(USER.getId(), COURSE.getId()); + + Assertions.assertEquals(certificates, result); + verify(certificateRepository).findCertificateByUserIdAndCourseId(USER.getId(), COURSE.getId()); + + } + + @Test + void delete() { + certificateService.delete(certificate.getId()); + + verify(certificateRepository).deleteById(certificate.getId()); + } + + @Test + void findAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<Certificate> page = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(certificateRepository.findAll(pageable)).thenReturn(page); + + Page<Certificate> result = certificateService.findAll(pageable); + + Assertions.assertEquals(page, result); + verify(certificateRepository).findAll(pageable); + + } + +} diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateApplicationTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateApplicationTests.java deleted file mode 100644 index a0c63b18d2153bee27df6cc4cf7306a2d8aa4bb4..0000000000000000000000000000000000000000 --- a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.fuseri.modulecertificate; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ModuleCertificateApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateCertificateControllerTests.java b/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateCertificateControllerTests.java deleted file mode 100644 index b6bf1c8694f821b1b4116761894d4566530f237f..0000000000000000000000000000000000000000 --- a/application/module-certificate/src/test/java/org/fuseri/modulecertificate/ModuleCertificateCertificateControllerTests.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.fuseri.modulecertificate; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.fuseri.model.dto.certificate.CertificateCreateDto; -import org.fuseri.model.dto.certificate.CertificateDto; -import org.fuseri.model.dto.course.CourseDto; -import org.fuseri.model.dto.course.LanguageTypeDto; -import org.fuseri.model.dto.course.ProficiencyLevelDto; -import org.fuseri.model.dto.user.AddressDto; -import org.fuseri.model.dto.user.UserDto; -import org.fuseri.modulecertificate.service.CertificateController; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - - -@SpringBootTest -@AutoConfigureMockMvc -class ModuleCertificateCertificateControllerTests { - - @Autowired - private MockMvc mockMvc; - - @Autowired - ObjectMapper objectMapper; - - @Test - void generateCertificate() throws Exception { - CertificateDto expectedResponse = new CertificateDto(); - String response = mockMvc.perform(post("/certificates/generate") - .content(asJsonString(new CertificateCreateDto( - new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()), - new CourseDto("AJ1", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1)))) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - CertificateController certificateController = objectMapper.readValue(response, CertificateController.class); - assertThat("response", certificateController.find("0"), is(instanceOf(expectedResponse.getClass()))); - } - - - @Test - void deleteCertificate() throws Exception { - String response = mockMvc.perform(delete("/certificates/delete") - .param("id", "1")) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - } - - public static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/application/module-exercise/Dockerfile b/application/module-exercise/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e31eb6c17a52ef6e28c4edbf02bed322397ab46 --- /dev/null +++ b/application/module-exercise/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-exercise-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java index 12fac75aedf87d7c1f29fe7333a24762d19a9ad3..9686c42ea06b4286bfdb2b7c1324cef02121761f 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/Answer.java @@ -1,7 +1,9 @@ package org.fuseri.moduleexercise.answer; +import jakarta.persistence.*; import lombok.*; import org.fuseri.moduleexercise.common.DomainObject; +import org.fuseri.moduleexercise.question.Question; /** * Represent Answer entity @@ -12,10 +14,16 @@ import org.fuseri.moduleexercise.common.DomainObject; @NoArgsConstructor @AllArgsConstructor @Builder +@Entity +@Table(name = "answer") public class Answer extends DomainObject { + private String text; + @Column(name = "is_correct") private boolean correct; - private String questionId; + @ManyToOne + @JoinColumn(name="question_id") + private Question question; } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java index 9f2f75a06870be2aa1bb7898a0caa471197f1681..b3829f98eff78333be094ae152fc0f6a5425d9d6 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java @@ -1,18 +1,25 @@ package org.fuseri.moduleexercise.answer; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; -import org.fuseri.model.dto.exercise.AnswersCreateDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import java.util.List; - /** * Represent a REST API controller for answers * Handle HTTP requests related to answers @@ -28,30 +35,24 @@ public class AnswerController { this.facade = facade; } - /** - * Retrieve a list of AnswerDto objects which belong to question with questionId - * - * @param questionId the ID of the question for which to retrieve answers - * @return a List of AnswerDto objects - */ - @GetMapping("/{question-id}") - public List<AnswerDto> findAllByQuestionId(@NotBlank @PathVariable("question-id") String questionId) { - return facade.findAllByQuestionId(questionId); - } - /** * Create a new answer for the given question ID * * @param dto the AnswerCreateDto object containing information about the answer to create - * @return an AnswerDto object representing the newly created answer - * @throws ResponseStatusException if the question ID specified in the dto does not exist + * @return a ResponseEntity containing an AnswerDto object representing the newly created answer, or a 404 Not Found response + * if the question with the specified ID in dto was not found */ + @Operation(summary = "Create new answer for question", description = "Creates new answer for question.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Answers created successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input.") + }) @PostMapping - public List<AnswerDto> createMultiple(@Valid @RequestBody AnswersCreateDto dto) { + public ResponseEntity<AnswerDto> create(@Valid @RequestBody AnswerCreateDto dto) { try { - return facade.createMultiple(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(facade.create(dto)); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + return ResponseEntity.notFound().build(); } } @@ -60,30 +61,42 @@ public class AnswerController { * * @param id of answer to update * @param dto dto with updated answer information - * @throws ResponseStatusException if the question id specified in the AnswerCreateDto dto does not exist + * @return A ResponseEntity with an AnswerDto object representing the updated answer on an HTTP status code of 200 if the update was successful. + * or a NOT_FOUND response if the answer ID is invalid */ + @Operation(summary = "Update an answer", description = "Updates an answer with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Answer with the specified ID updated successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input."), + @ApiResponse(responseCode = "404", description = "Answer with the specified ID was not found.") + }) @PutMapping("/{id}") - public AnswerDto update(@NotBlank @PathVariable String id, @Valid @RequestBody AnswerCreateDto dto) { + public ResponseEntity<AnswerDto> update(@NotNull @PathVariable long id, @Valid @RequestBody AnswerCreateDto dto) { try { - return facade.update(id, dto); + return ResponseEntity.ok(facade.update(id, dto)); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - e.getMessage()); + return ResponseEntity.notFound().build(); } } /** - * Delete answer with the given id + * Delete answer with the specified ID * * @param id of answer to delete * @throws ResponseStatusException if answer with specified id does not exist */ + @Operation(summary = "Delete an answer with specified ID", description = "Deletes an answer with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Answer with the specified ID deleted successfully."), + @ApiResponse(responseCode = "404", description = "Answer with the specified ID was not found.") + }) @DeleteMapping("/{id}") - public void delete(@NotBlank @PathVariable String id) { + public ResponseEntity<Void> delete(@NotNull @PathVariable long id) { try { facade.delete(id); + return ResponseEntity.noContent().build(); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + return ResponseEntity.notFound().build(); } } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java index 9ef573dca41c6d90e1396484a1c54e29b1730073..693c8322b6feddc366c372f6fdce6fd8505f37b7 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerFacade.java @@ -1,95 +1,52 @@ package org.fuseri.moduleexercise.answer; +import jakarta.transaction.Transactional; import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; -import org.fuseri.model.dto.exercise.AnswersCreateDto; -import org.fuseri.moduleexercise.question.Question; -import org.fuseri.moduleexercise.question.QuestionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; +import org.springframework.web.bind.annotation.RequestBody; /** * Represent facade for managing answers * Provide simplified interface for manipulating with answers */ @Service +@Transactional public class AnswerFacade { private final AnswerService answerService; - private final QuestionService questionService; private final AnswerMapper mapper; - /** * Constructor for AnswerFacade * - * @param answerService the service responsible for handling answer-related logic - * @param questionService the service responsible for handling question-related logic - * @param mapper the mapper responsible for converting between DTOs and entities + * @param answerService the service responsible for handling answer-related logic + * @param mapper the mapper responsible for converting between DTOs and entities */ @Autowired - public AnswerFacade(AnswerService answerService, QuestionService questionService, AnswerMapper mapper) { + public AnswerFacade(AnswerService answerService, AnswerMapper mapper) { this.answerService = answerService; - this.questionService = questionService; this.mapper = mapper; } - /** - * Retrieve a list of AnswerDto objects which belong to question with questionId - * - * @param questionId the ID of the question for which to retrieve answers - * @return a List of AnswerDto objects - */ - public List<AnswerDto> findAllByQuestionId(String questionId) { - return mapper.toDtoList(answerService.findAllByQuestionId(questionId)); - } - /** * Create a new answer for the given question ID * * @param dto the AnswerCreateDto object containing information about the answer to create * @return an AnswerDto object representing the newly created answer */ - public List<AnswerDto> createMultiple(@RequestBody AnswersCreateDto dto) { - List<Answer> createdAnswers = new ArrayList<>(); - for (var answerDto : dto.getAnswers()) { - Question question; - question = questionService.find(dto.getQuestionId()); - - Answer answer = mapper.fromCreateDto(answerDto); - answer.setQuestionId(question.getId()); - var createdAnswer = answerService.create(answer); - question.getAnswers().add(answer); - createdAnswers.add(createdAnswer); - } - - return mapper.toDtoList(createdAnswers); + public AnswerDto create(@RequestBody AnswerCreateDto dto) { + return mapper.toDto(answerService.create(mapper.fromCreateDto(dto))); } /** - * Update an answer + * Update answer * * @param id of answer to update * @param dto dto with updated answer information */ - public AnswerDto update(String id, AnswerCreateDto dto) { - var updatedAnswer = mapper.fromCreateDto(dto); - updatedAnswer.setId(id); - answerService.update(updatedAnswer); - - Question question; - question = questionService.find(dto.getQuestionId()); - - var questionAnswers = question.getAnswers(); - questionAnswers.removeIf(a -> a.getId().equals(id)); - questionAnswers.add(updatedAnswer); - question.setAnswers(questionAnswers); - questionService.update(question); - - return mapper.toDto(updatedAnswer); + public AnswerDto update(long id, AnswerCreateDto dto) { + return mapper.toDto(answerService.update(id, mapper.fromCreateDto(dto))); } /** @@ -97,16 +54,7 @@ public class AnswerFacade { * * @param id of answer to delete */ - public void delete(String id) { - var answer = answerService.find(id); - - Question question; - question = questionService.find(answer.getQuestionId()); - - var questionAnswers = question.getAnswers(); - questionAnswers.removeIf(a -> a.getId().equals(answer.getId())); - question.setAnswers(questionAnswers); - + public void delete(long id) { answerService.delete(id); } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java index 3dec5ca627e8808d54e7f27593c834a73a0f5fa5..44b8d1f80ec4e4983e82bd107d308f11ba89f9b6 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerMapper.java @@ -4,13 +4,29 @@ import org.fuseri.model.dto.exercise.AnswerCreateDto; import org.fuseri.model.dto.exercise.AnswerDto; import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.moduleexercise.common.DomainMapper; +import org.fuseri.moduleexercise.question.Question; +import org.fuseri.moduleexercise.question.QuestionService; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Set; /** * Mapper between Answers and their corresponding DTOs */ -@Mapper -public interface AnswerMapper extends DomainMapper<Answer, AnswerDto> { +@Mapper(componentModel = "spring", uses = {QuestionService.class}) +public abstract class AnswerMapper implements DomainMapper<Answer, AnswerDto> { + + @Autowired + private QuestionService questionService; + + @Named("mapIdToQuestion") + public Question mapIdToQuestion(Long id) { + return questionService.find(id); + } /** * Convert DTO of type AnswerCreateDto to Answer @@ -18,12 +34,22 @@ public interface AnswerMapper extends DomainMapper<Answer, AnswerDto> { * @param dto DTO to be converted * @return corresponding Answer entity created from DTO */ - Answer fromCreateDto(AnswerCreateDto dto); + @Mapping(target = "question", source = "dto.questionId", qualifiedByName = "mapIdToQuestion") + public abstract Answer fromCreateDto(AnswerCreateDto dto); + + /** + * Convert Set of AnswerInQuestionCreateDto to List of corresponding Answers + * + * @param dtos to be converted + * @return corresponding list of Answers + */ + public abstract Set<Answer> fromCreateDtoList(List<AnswerInQuestionCreateDto> dtos); /** * Convert DTO of type AnswerInQuestionCreateDto to Answer + * * @param dto DTO to be converted * @return corresponding Answer entity created from DTO */ - Answer fromCreateDto(AnswerInQuestionCreateDto dto); + public abstract Answer fromCreateDto(AnswerInQuestionCreateDto dto); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java index 7f1918988b192f6c0f0f5fe54f92544d4d72dcbf..8d0844a898915658d73220923586a5630ac351a7 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepository.java @@ -1,14 +1,16 @@ package org.fuseri.moduleexercise.answer; -import org.fuseri.moduleexercise.common.DomainRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; /** * A repository interface for managing Answer entities */ -public interface AnswerRepository extends DomainRepository<Answer, String> { +@Repository +public interface AnswerRepository extends JpaRepository<Answer, Long> { /** * Find all answers to a question with the specified ID @@ -16,5 +18,5 @@ public interface AnswerRepository extends DomainRepository<Answer, String> { * @param questionId the ID of the question to find answers for * @return a list of all answers to the specified question */ - List<Answer> findByQuestionId(@Param("questionId") String questionId); + List<Answer> findByQuestionId(@Param("questionId") long questionId); } \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java deleted file mode 100644 index 7a75a834add8e358abb04aa628ad9885a80d5705..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerRepositoryImpl.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.fuseri.moduleexercise.answer; - -import org.fuseri.moduleexercise.common.DomainRepositoryImpl; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * An implementation of the AnswerRepository interface - * Provides access to Answer entities stored in a data source - */ -@Repository -public class AnswerRepositoryImpl extends DomainRepositoryImpl<Answer> implements AnswerRepository { - - /** - * Find all answers to a question with the specified ID - * - * @param questionId the ID of the question to find answers for - * @return a list of all answers to the specified question - */ - @Override - public List<Answer> findByQuestionId(String questionId) { - return getItems() - .stream() - .filter(e -> e.getQuestionId().equals(questionId)) - .toList(); - } -} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java index 593656acfea40265f835d91d6b5f2572a8bdea31..f5346fc5993874b4de7ca7d49fe39ba8c17ee6ba 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java @@ -36,9 +36,13 @@ public class AnswerService extends DomainService<Answer> { * * @param questionId the ID of the question to retrieve answers for * @return a list of Answer entities with the specified question ID + * @throws EntityNotFoundException if question with questionId does not exist */ @Transactional(readOnly = true) - public List<Answer> findAllByQuestionId(String questionId) { + public List<Answer> findAllByQuestionId(long questionId) { + if (!getRepository().existsById(questionId)) { + throw new EntityNotFoundException("Question with id " + questionId + " not found."); + } return repository.findByQuestionId(questionId); } @@ -50,7 +54,7 @@ public class AnswerService extends DomainService<Answer> { * @throws EntityNotFoundException if no Answer entity exists with the specified id */ @Transactional(readOnly = true) - public Answer find(String id) { + public Answer find(long id) { return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Answer '" + id + "' not found.")); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java index 91151e34a7604b75d8ec6e7ef3a4767bbfff811a..39bf0015e1cacc60a34a78e804a77ce18ccc6875 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainMapper.java @@ -1,10 +1,8 @@ package org.fuseri.moduleexercise.common; import org.fuseri.model.dto.common.DomainObjectDto; -import org.fuseri.model.dto.common.Result; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import java.util.List; @@ -42,18 +40,13 @@ public interface DomainMapper<T extends DomainObject, S extends DomainObjectDto> List<S> toDtoList(List<T> entities); /** - * Convert a {@link org.springframework.data.domain.Page} containing entities to a - * {@link Result} object containing a list of their corresponding DTO objects, along with - * pagination information + * Convert a {@link org.springframework.data.domain.Page} containing entities to a page containing DTOs * - * @param source the Page of entities to be converted - * @return a Result object containing a list of DTO objects and pagination information + * @param entities entities to be converted to DTOs + * @return a Page object containing a list of DTO objects and pagination information */ - @Mappings({ - @Mapping(target = "total", expression = "java(source.getTotalElements())"), - @Mapping(target = "page", expression = "java(source.getNumber())"), - @Mapping(target = "pageSize", expression = "java(source.getSize())"), - @Mapping(target = "items", expression = "java(toDtoList(source.getContent()))") - }) - Result<S> toResult(Page<T> source); + default Page<S> toDtoPage(Page<T> entities) { + return new PageImpl<>(toDtoList(entities.getContent()), entities.getPageable(), entities.getTotalPages()); + } + } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java index 077a8e5cdb521f4c233572accf0e6a8bb10ac4d3..aae4e7759835653b19afad27db311e8d23095515 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainObject.java @@ -1,12 +1,12 @@ package org.fuseri.moduleexercise.common; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import lombok.Getter; import lombok.Setter; -import java.util.UUID; - /** * Represent the base class for entities in the module. */ @@ -16,6 +16,7 @@ import java.util.UUID; public abstract class DomainObject { @Id - private String id = UUID.randomUUID().toString(); + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java deleted file mode 100644 index fae42adfae3eabea93352b01a26b25d36418bee1..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.fuseri.moduleexercise.common; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; - -import java.util.Optional; - -/** - * Dummy interface of repository. Later will be replaced by JpaRepository. - * - * @param <T> entity - * @param <ID> entity ID - */ -public interface DomainRepository<T, ID> { - - /** - * Save the specified entity - * - * @param entity entity to be saved - * @return created entity - */ - T save(T entity); - - /** - * Find entity by ID - * - * @param id ID of entity to be found - * @return {@code Optional} containing the found entity, - * or an empty {@code Optional} if no such entity exists - */ - Optional<T> findById(ID id); - - /** - * Retrieve a page of entities according to the specified pagination information - * - * @param pageRequest the pagination information for the query - * @return a page of entities that satisfy the pagination criteria - */ - Page<T> findAll(PageRequest pageRequest); - - /** - * Update entity - * - * @param entity entity to update - * @return updated entity - */ - T update(T entity); - - /** - * Delete the entity with the specified id - * Note that this does not do cascade deleting. - * We will have cascade deleting with usage of JpaRepository - * - * @param id the id of the entity to be deleted - */ - void deleteById(ID id); -} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java deleted file mode 100644 index 5c6b588fa6f6396062c93acc959d2d823284c129..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainRepositoryImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.fuseri.moduleexercise.common; - -import jakarta.persistence.EntityNotFoundException; -import lombok.Getter; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; - -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * Dummy implementation of repository. Later will be replaced by JpaRepository. - * - * @param <T> entity - */ -public abstract class DomainRepositoryImpl<T extends DomainObject> implements DomainRepository<T, String> { - - /** - * Dummy database - */ - @Getter - private final Set<T> items = new HashSet<>(); - - /** - * Save the specified entity - * - * @param entity entity to be saved - * @return created entity - */ - @Override - public T save(T entity) { - items.add(entity); - return entity; - } - - /** - * Find entity by ID - * - * @param id ID of entity to be found - * @return {@code Optional} containing the found entity, - * or an empty {@code Optional} if no such entity exists - */ - @Override - public Optional<T> findById(String id) { - return items.stream() - .filter(e -> e.getId().equals(id)) - .findFirst(); - } - - /** - * Retrieve a page of entities according to the specified pagination information - * - * @param pageRequest the pagination information for the query - * @return a page of entities that satisfy the pagination criteria - */ - @Override - public Page<T> findAll(PageRequest pageRequest) { - - int startIndex = pageRequest.getPageNumber() * pageRequest.getPageSize(); - - List<T> pageEntities = items.stream() - .skip(startIndex) - .limit(pageRequest.getPageSize()) - .toList(); - - return new PageImpl<>(pageEntities, pageRequest, pageEntities.size()); - } - - /** - * Update entity - * - * @param entity entity to update - * @return updated entity - */ - @Override - public T update(T entity) { - if (entity == null || entity.getId() == null) { - throw new IllegalArgumentException("Entity and its ID can not be null."); - } - - var optionalEntity = findById(entity.getId()); - if (optionalEntity.isEmpty()) { - throw new EntityNotFoundException("Entity not found with ID: " + entity.getId()); - } - - T oldEntity = optionalEntity.get(); - items.remove(oldEntity); - items.add(entity); - return entity; - } - - /** - * Delete the entity with the specified id - * Note that this does not do cascade deleting. - * We will have cascade deleting with usage of JpaRepository - * - * @param id the id of the entity to be deleted - */ - @Override - public void deleteById(String id) { - items.removeIf(e -> e.getId().equals(id)); - } -} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java index badd21263def20ee0cf660c2be3f1f156a84aed4..e0824dae22c74ed937ed4a1cbda4dda48a16b912 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/common/DomainService.java @@ -1,5 +1,8 @@ package org.fuseri.moduleexercise.common; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; + /** * Represent common service for managing entities * @@ -17,7 +20,7 @@ public abstract class DomainService<T extends DomainObject> { * * @return the repository used by this service */ - public abstract DomainRepository<T, String> getRepository(); + public abstract JpaRepository<T, Long> getRepository(); /** * Create an entity by saving it to the repository @@ -32,18 +35,24 @@ public abstract class DomainService<T extends DomainObject> { /** * Update an entity * + * @param id the entity ID * @param entity the entity to update * @return the updated entity */ - public T update(T entity) { - return getRepository().update(entity); + public T update(long id, T entity) { + if (!getRepository().existsById(id)) { + throw new EntityNotFoundException("Entity with id " + entity.getId() + " not found."); + } + entity.setId(id); + return getRepository().save(entity); } /** * Delete an entity with specified id * @param id id of the entity to delete */ - public void delete(String id) { + public void delete(long id) { getRepository().deleteById(id); } + } \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java index 65ff0e59f9a31773168eaf378000bac45f010fd0..6070b8a70b26a36f3e87b4f4661a43a64625cfe5 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java @@ -1,5 +1,6 @@ package org.fuseri.moduleexercise.exercise; +import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,6 +19,8 @@ import java.util.Set; @Setter @NoArgsConstructor @Builder +@Entity +@Table(name = "exercise") public class Exercise extends DomainObject { private String name; @@ -26,8 +29,10 @@ public class Exercise extends DomainObject { private int difficulty; - private String courseId; + @Column(name = "lecture_id") + private long courseId; + @OneToMany(mappedBy="exercise", cascade = CascadeType.ALL) private Set<Question> questions = new HashSet<>(); /** @@ -39,7 +44,7 @@ public class Exercise extends DomainObject { * @param courseId id of lecture to which exercise belongs * @param questions question exercise contains */ - public Exercise(String name, String description, int difficulty, String courseId, Set<Question> questions) { + public Exercise(String name, String description, int difficulty, long courseId, Set<Question> questions) { this.name = name; this.description = description; this.difficulty = difficulty; diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java index 1a53e6f9630c53ba29d16275f982202628181fbf..96f91fc77b92040b231cc51218ff393a26184530 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java @@ -1,17 +1,28 @@ package org.fuseri.moduleexercise.exercise; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; -import org.fuseri.model.dto.common.Result; import org.fuseri.model.dto.exercise.ExerciseCreateDto; import org.fuseri.model.dto.exercise.ExerciseDto; +import org.fuseri.model.dto.exercise.QuestionDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * Represent a REST API controller for exercises @@ -21,104 +32,153 @@ import org.springframework.web.server.ResponseStatusException; @RequestMapping("/exercises") public class ExerciseController { - private final ExerciseService service; - - private final ExerciseMapper mapper; + private final ExerciseFacade facade; /** - * Constructor for AnswerController + * Constructor for ExerciseController * - * @param service the service responsible for handling exercise-related logic - * @param mapper the mapper responsible for converting between DTOs and entities + * @param facade the facade responsible for handling exercise-related logic */ @Autowired - public ExerciseController(ExerciseService service, ExerciseMapper mapper) { - this.service = service; - this.mapper = mapper; + public ExerciseController(ExerciseFacade facade) { + this.facade = facade; } /** - * Create a new answer for the given question ID + * Create a new exercise * - * @param dto the ExerciseCreateDto object containing information about the exercise to create - * @return an ExerciseDto object representing the newly created exercise + * @param dto containing information about the exercise to create + * @return a ResponseEntity containing an ExerciseDto object representing the newly created exercise */ + @Operation(summary = "Create an exercise", description = "Creates a new exercise.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Exercise created successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input.") + }) @PostMapping - public ExerciseDto create(@Valid @RequestBody ExerciseCreateDto dto) { - Exercise exercise = mapper.fromCreateDto(dto); - return mapper.toDto(service.create(exercise)); + public ResponseEntity<ExerciseDto> create(@Valid @RequestBody ExerciseCreateDto dto) { + ExerciseDto exerciseDto = facade.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(exerciseDto); } /** * Find an exercise by ID * * @param id the ID of the exercise to find - * @return an ExerciseDto object representing the found exercise + * @return a ResponseEntity containing an ExerciseDto object representing the found exercise, or a 404 Not Found response + * if the exercise with the specified ID was not found */ + @Operation(summary = "Get an exercise by ID", description = "Returns an exercise with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Exercise with the specified ID retrieved successfully."), + @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") + }) @GetMapping("/{id}") - public ExerciseDto find(@NotBlank @PathVariable String id) { - return mapper.toDto(service.find(id)); + public ResponseEntity<ExerciseDto> find(@NotNull @PathVariable long id) { + try { + return ResponseEntity.ok(facade.find(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } /** - * Find exercises and return them in a paginated format + * Find exercises and return them in paginated format * * @param page the page number of the exercises to retrieve - * @return a Result object containing a list of ExerciseDto objects and pagination information + * @return A ResponseEntity containing paginated ExerciseDTOs. */ + @Operation(summary = "Get exercises in paginated format", description = "Returns exercises in paginated format.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved paginated exercises"), + @ApiResponse(responseCode = "400", description = "Invalid page number supplied"), + }) @GetMapping - public Result<ExerciseDto> findAll(@PositiveOrZero @RequestParam int page) { - Page<Exercise> exercise = service.findAll(page); - return mapper.toResult(exercise); + public ResponseEntity<Page<ExerciseDto>> findAll(@PositiveOrZero @RequestParam int page) { + return ResponseEntity.ok(facade.findAll(page)); } /** * Find exercises that mach filters and return them in paginated format * - * @param page the page number of the exercises to retrieve * @param courseId the id of the course to filter by * @param difficulty the difficulty level to filter by - * @return a Result object containing a list of filtered ExerciseDto objects woth pagination information + * @param page the page number of the exercises to retrieve + * @return A ResponseEntity containing filtered and paginated ExerciseDTOs */ + @Operation(summary = "Filter exercises per difficulty and per course", description = "Returns exercises which belong to specified course and have specified difficulty.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved filtered paginated exercises."), + }) @GetMapping("filter") - public Result<ExerciseDto> findPerDifficultyPerCourse( - @PositiveOrZero @RequestParam int page, @NotBlank @RequestParam String courseId, + public ResponseEntity<Page<ExerciseDto>> findPerDifficultyPerCourse( + @PositiveOrZero @RequestParam int page, @NotNull @RequestParam long courseId, @PositiveOrZero @RequestParam int difficulty) { - Page<Exercise> exercise = service.findPerDifficultyPerCourse(page, courseId, difficulty); - return mapper.toResult(exercise); + Page<ExerciseDto> exercises = facade.findByCourseIdAndDifficulty(courseId, difficulty, page); + return ResponseEntity.ok(exercises); + } + + /** + * Find questions by exercise ID and return them in a paginated format + * + * @param exerciseId the ID of the exercise to find questions for + * @param page the page number of the questions to retrieve + * @return a ResponseEntity containing paginated QuestionDTOs which belong to an exercise with exerciseId + * or a NOT_FOUND response if the exercise ID is invalid + */ + @Operation(summary = "Find questions belonging to exercise by exercise ID", + description = "Returns a paginated list of questions for the specified exercise ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Questions found and returned successfully."), + @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") + }) + @GetMapping("/{exercise-id}/questions") + public ResponseEntity<Page<QuestionDto>> findQuestions(@NotNull @PathVariable("exercise-id") long exerciseId, + @PositiveOrZero @RequestParam int page) { + try { + Page<QuestionDto> questions = facade.getQuestions(exerciseId, page); + return ResponseEntity.ok(questions); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } /** - * Update an exercise with id + * Update an exercise with ID * * @param id the ID of the exercise to update * @param dto the ExerciseCreateDto object containing information about the exercise to update - * @return an ExerciseDto object representing the updated exercise - * @throws ResponseStatusException invalid exercise id + * @return A ResponseEntity with an ExerciseDto object representing the updated exercise an HTTP status code of 200 if the update was successful. + * or a NOT_FOUND response if the exercise ID is invalid */ - + @Operation(summary = "Update a exercise", description = "Updates a exercise with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Exercise with the specified ID updated successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input."), + @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") + }) @PutMapping("/{id}") - public ExerciseDto update(@NotBlank @PathVariable String id, @Valid @RequestBody ExerciseCreateDto dto) { - Exercise exercise = mapper.fromCreateDto(dto); - exercise.setId(id); - + public ResponseEntity<ExerciseDto> update(@NotNull @PathVariable long id, @Valid @RequestBody ExerciseCreateDto dto) { try { - return mapper.toDto(service.update(exercise)); - } catch (IllegalArgumentException e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + return ResponseEntity.ok(facade.update(id, dto)); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + return ResponseEntity.notFound().build(); } } /** - * Delete an exercise with exerciseId + * Delete an exercise with ID * * @param id the ID of the exercise to delete */ + @Operation(summary = "Delete a exercise with specified ID", description = "Deletes a exercise with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Exercise with the specified ID deleted successfully."), + }) @DeleteMapping("/{id}") - public void delete(@NotBlank @PathVariable String id) { - service.delete(id); + public ResponseEntity<Void> delete(@NotNull @PathVariable long id) { + facade.delete(id); + return ResponseEntity.noContent().build(); } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..3134fadf5997e659f224d894f775c1df19e1ec90 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseFacade.java @@ -0,0 +1,121 @@ +package org.fuseri.moduleexercise.exercise; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.fuseri.model.dto.exercise.QuestionDto; +import org.fuseri.moduleexercise.question.Question; +import org.fuseri.moduleexercise.question.QuestionMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +/** + * Represent facade for managing exercises + * Provide simplified interface for manipulating with exercises + */ +@Service +@Transactional +public class ExerciseFacade { + private final ExerciseService exerciseService; + private final ExerciseMapper exerciseMapper; + private final QuestionMapper questionMapper; + + /** + * Constructor for AnswerFacade + * + * @param exerciseService the service responsible for handling answer-related logic + * @param exerciseMapper the mapper responsible for converting between DTOs and entities + */ + @Autowired + public ExerciseFacade(ExerciseService exerciseService, ExerciseMapper exerciseMapper, QuestionMapper questionMapper) { + this.exerciseService = exerciseService; + this.exerciseMapper = exerciseMapper; + this.questionMapper = questionMapper; + } + + /** + * Create a new exercise + * + * @param exerciseDto dto with information from which exercise is created + * @return a created exercise dto + */ + public ExerciseDto create(ExerciseCreateDto exerciseDto) { + Exercise exerciseToCreate = exerciseMapper.fromCreateDto(exerciseDto); + Exercise createdExercise = exerciseService.create(exerciseToCreate); + return exerciseMapper.toDto(createdExercise); + } + + /** + * Retrieve the Exercise entity with the specified ID + * + * @param id the ID of the Exercise entity to retrieve + * @return the Exercise entity with the specified ID + * @throws EntityNotFoundException if no Exercise entity exists with the specified ID + */ + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public ExerciseDto find(long id) { + return exerciseMapper.toDto(exerciseService.find(id)); + } + + /** + * Retrieve a page of Exercise entities + * + * @param page the page number to retrieve (0-indexed) + * @return paginated exerciseDTOs + */ + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public Page<ExerciseDto> findAll(int page) { + return exerciseMapper.toDtoPage(exerciseService.findAll(page)); + } + + /** + * Retrieve a page of exercises filtered by the specified course id and difficulty level + * + * @param page the page number to retrieve + * @param courseId the id of the course to filter by + * @param difficulty the difficulty level to filter by + * @return paginated exerciseDTOs filtered by the specified course ID and difficulty level + */ + public Page<ExerciseDto> findByCourseIdAndDifficulty(long courseId, int difficulty, int page) { + Page<Exercise> exercises = exerciseService.findByCourseIdAndDifficulty(courseId, difficulty, page); + return exerciseMapper.toDtoPage(exercises); + } + + /** + * Retrieve a page of Question entities associated with the specified exercise ID + * + * @param exerciseId the ID of the exercise to retrieve questions for + * @param page the page number to retrieve (0-indexed) + * @return paginated questionDTOs associated with the specified exercise ID + */ + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public Page<QuestionDto> getQuestions(long exerciseId, int page) { + Page<Question> questions = exerciseService.getQuestions(exerciseId, page); + return questionMapper.toDtoPage(questions); + } + + /** + * Update exercise + * + * @param id the ID of the exercise to update + * @param dto the ExerciseCreateDto object containing information about the exercise to update + * @return an ExerciseDto object representing the updated exercise + */ + public ExerciseDto update(long id, ExerciseCreateDto dto) { + Exercise exercise = exerciseMapper.fromCreateDto(dto); + Exercise updatedExercise = exerciseService.update(id, exercise); + return exerciseMapper.toDto(updatedExercise); + } + + /** + * Delete exercise + * + * @param id of exercise to delete + */ + public void delete(long id) { + exerciseService.delete(id); + } + +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java index 3c211aaa5d034ecba3b54e738da0ebb4315b51bf..3b44db86873b1f10c1911149a9a018f25dfb423a 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseMapper.java @@ -1,9 +1,14 @@ package org.fuseri.moduleexercise.exercise; +import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.exercise.ExerciseCreateDto; import org.fuseri.model.dto.exercise.ExerciseDto; import org.fuseri.moduleexercise.common.DomainMapper; import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; /** * Mapper between Exercises and their corresponding DTOs @@ -18,4 +23,10 @@ public interface ExerciseMapper extends DomainMapper<Exercise, ExerciseDto> { * @return corresponding Exercise entity created from DTO */ Exercise fromCreateDto(ExerciseCreateDto dto); + + List<ExerciseDto> mapToList(List<Exercise> persons); + + default Page<ExerciseDto> mapToPageDto(Page<Exercise> courses) { + return new PageImpl<>(mapToList(courses.getContent()), courses.getPageable(), courses.getTotalPages()); + } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java index 76ec2a3a1eaf05eb979ccd4cdd8d5267397addb3..a2ff556892065d2be6b365566bf3bac9a6e0a504 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepository.java @@ -1,24 +1,34 @@ package org.fuseri.moduleexercise.exercise; -import org.fuseri.model.dto.common.Result; -import org.fuseri.moduleexercise.common.DomainRepository; +import org.fuseri.moduleexercise.question.Question; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; /** * A repository interface for managing Exercise entities */ -public interface ExerciseRepository extends DomainRepository<Exercise, String> { +@Repository +public interface ExerciseRepository extends JpaRepository<Exercise, Long> { /** * Filters the exercises by the specified difficulty level and course id, - * and returns a {@link Result} object containing these filtered exercises + * and returns an object containing these filtered exercises * along with pagination information * - * @param pageRequest the pagination settings for the result - * @param courseId the id of the course to filter by - * @param difficulty the difficulty level to filter by - * @return a {@link Result} object containing a list of paginated exercises that match the filter criteria + * @param courseId the id of the course to filter by + * @param difficulty the difficulty level to filter by + * @param pageable the pagination settings + * @return object containing a list of paginated exercises that match the filter criteria */ - Page<Exercise> filterPerDifficultyPerCourse(PageRequest pageRequest, String courseId, int difficulty); + @Query("SELECT e FROM Exercise e WHERE e.courseId = :courseId AND e.difficulty = :difficulty") + Page<Exercise> findByCourseIdAndDifficulty(long courseId, int difficulty, Pageable pageable); + + @Query("SELECT q FROM Exercise e JOIN e.questions q WHERE e.id = :exerciseId") + Page<Question> getQuestions(PageRequest pageRequest, @Param("exerciseId") Long exerciseId); + } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java deleted file mode 100644 index 3036f93b02043658d38c332c45d2cbc154f308c6..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.fuseri.moduleexercise.exercise; - -import org.fuseri.model.dto.common.Result; -import org.fuseri.moduleexercise.common.DomainRepositoryImpl; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * An implementation of the ExerciseRepository interface - * Provides access to Exercise entities stored in a data source - */ -@Repository -public class ExerciseRepositoryImpl extends DomainRepositoryImpl<Exercise> implements ExerciseRepository { - - /** - * Filters the exercises by the specified difficulty level and course id, - * and returns a {@link Result} object containing these filtered exercises - * along with pagination information - * - * @param pageRequest the pagination settings for the result - * @param courseId the id of the course to filter by - * @param difficulty the difficulty level to filter by - * @return a {@link Result} object containing a list of paginated exercises that match the filter criteria - */ - @Override - public Page<Exercise> filterPerDifficultyPerCourse(PageRequest pageRequest, String courseId, int difficulty) { - - int startIndex = pageRequest.getPageNumber() * pageRequest.getPageSize(); - - List<Exercise> pageEntities = getItems().stream() - .filter(e -> e.getCourseId().equals(courseId) && e.getDifficulty() == difficulty) - .skip(startIndex) - .limit(pageRequest.getPageSize()) - .toList(); - - return new PageImpl<>(pageEntities, pageRequest, pageEntities.size()); - } -} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java index 677c704a7892b13672c8534c0d731d3ba7a2abb6..41aa3c7e68a9c05b82fe0634d17c481525451d7c 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseService.java @@ -3,6 +3,7 @@ package org.fuseri.moduleexercise.exercise; import jakarta.persistence.EntityNotFoundException; import lombok.Getter; import org.fuseri.moduleexercise.common.DomainService; +import org.fuseri.moduleexercise.question.Question; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -39,7 +40,7 @@ public class ExerciseService extends DomainService<Exercise> { * @throws EntityNotFoundException if no Exercise entity exists with the specified ID */ @Transactional(readOnly = true) - public Exercise find(String id) { + public Exercise find(long id) { return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Exercise '" + id + "' not found.")); } @@ -58,13 +59,30 @@ public class ExerciseService extends DomainService<Exercise> { /** * Retrieve a page of exercises filtered by the specified course id and difficulty level * - * @param page the page number to retrieve * @param courseId the id of the course to filter by * @param difficulty the difficulty level to filter by - * @return a {@link Page} of {@link Exercise} objects filtered by the specified course id and difficulty level + * @param page the page number to retrieve + * @return paginated exercises filtered by the specified course ID and difficulty level */ - public Page<Exercise> findPerDifficultyPerCourse(int page, String courseId, int difficulty) { - return repository.filterPerDifficultyPerCourse( - PageRequest.of(page, DEFAULT_PAGE_SIZE), courseId, difficulty); + public Page<Exercise> findByCourseIdAndDifficulty(long courseId, int difficulty, int page) { + return repository.findByCourseIdAndDifficulty(courseId, difficulty, PageRequest.of(page, DEFAULT_PAGE_SIZE)); } + + /** + * Retrieve a page of Question entities associated with the specified exercise ID + * + * @param exerciseId the ID of the exercise to retrieve questions for + * @param page the page number to retrieve (0-indexed) + * @return a page of Question entities associated with the specified exercise ID + */ + @Transactional(readOnly = true) + public Page<Question> getQuestions(long exerciseId, int page) { + if (!repository.existsById(exerciseId)) { + throw new EntityNotFoundException("Exercise with ID " + exerciseId + "does not exist."); + } + return repository.getQuestions( + PageRequest.of(page, DomainService.DEFAULT_PAGE_SIZE), + exerciseId); + } + } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java index 042604af6ad1e339be34b38f5481806938dfbb38..03f62910b63062589484675c9f4d1ba436ec60c5 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java @@ -1,14 +1,21 @@ package org.fuseri.moduleexercise.question; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.common.DomainObject; +import org.fuseri.moduleexercise.exercise.Exercise; import java.util.HashSet; -import java.util.Objects; import java.util.Set; /** @@ -17,25 +24,27 @@ import java.util.Set; @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @Builder +@Entity +@Table(name = "question") public class Question extends DomainObject { private String text; + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Answer> answers = new HashSet<>(); - private String exerciseId; + @ManyToOne + @JoinColumn(name = "exercise_id", nullable = false) + private Exercise exercise; /** - * Constructor of question + * Add answers to question * - * @param text question text - * @param answers question answers - * @param exerciseId id of exercise the question belongs to + * @param answersToAdd to add */ - public Question(String text, Set<Answer> answers, String exerciseId) { - this.text = text; - this.answers = Objects.requireNonNullElseGet(answers, HashSet::new); - this.exerciseId = exerciseId; + public void addAnswers(Set<Answer> answersToAdd) { + answers.addAll(answersToAdd); } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java index bd217cc1c00f36d02b1b074b6e9028c20e291220..9857e1c3d46120979b04339b2ddf0293c22c0ed8 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java @@ -1,17 +1,32 @@ package org.fuseri.moduleexercise.question; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PositiveOrZero; -import org.fuseri.model.dto.common.Result; +import jakarta.validation.constraints.NotNull; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; /** * Represent a REST API controller for questions @@ -37,40 +52,64 @@ public class QuestionController { * Find a question by ID. * * @param id the ID of the question to find - * @return a QuestionDto object representing the found question + * @return a ResponseEntity containing a QuestionDto object representing the found question, or a 404 Not Found response + * if the question with the specified ID was not found */ + @Operation(summary = "Get a question by ID", description = "Returns a question with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Question with the specified ID retrieved successfully.", + content = @Content(schema = @Schema(implementation = QuestionDto.class))), + @ApiResponse(responseCode = "404", description = "Question with the specified ID was not found.") + }) @GetMapping("/{id}") - public QuestionDto find(@NotBlank @PathVariable String id) { - return questionFacade.find(id); + public ResponseEntity<QuestionDto> find(@NotNull @PathVariable long id) { + try { + return ResponseEntity.ok(questionFacade.find(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } /** - * Find questions by exercise ID and return them in a paginated format + * Retrieve a list of AnswerDto objects which belong to the question with ID * - * @param exerciseId the ID of the exercise to find questions for - * @param page the page number of the questions to retrieve - * @return a Result object containing a list of QuestionDto objects and pagination information + * @param id the ID of the question for which to retrieve answers + * @return a ResponseEntity containing a List of AnswerDto objects, or a 404 Not Found response + * if the question with the specified ID was not found */ - @GetMapping("/exercise/{exercise-id}") - public Result<QuestionDto> findByExerciseId(@NotBlank @PathVariable("exercise-id") String exerciseId, - @PositiveOrZero @RequestParam int page) { - return questionFacade.findByExerciseId(exerciseId, page); + @Operation(summary = "Retrieve answers for a specific question") + @ApiResponse(responseCode = "200", description = "Successfully retrieved answers", + content = @Content(schema = @Schema(implementation = AnswerDto.class))) + @ApiResponse(responseCode = "404", description = "Question not found") + @GetMapping("/{id}/answers") + public ResponseEntity<List<AnswerDto>> getQuestionAnswers(@NotNull @PathVariable long id) { + try { + return ResponseEntity.ok(questionFacade.getQuestionAnswers(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } /** * Add a new question to an exercise * * @param dto a QuestionCreateDto object representing the new question to add - * @return a QuestionDto object representing the added question - * @throws ResponseStatusException if the exercise with exerciseId from QuestionCreateDto does not exist + * @return a ResponseEntity containing a QuestionDto object representing the posted question, or a 404 Not Found response + * if the exercise with the specified ID in dto was not found */ + @Operation(summary = "Add a new question with answers to an exercise", description = "Creates a new question with answers and associates it with the specified exercise.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Question with answers created and added to the exercise successfully.", + content = @Content(schema = @Schema(implementation = QuestionDto.class))), + @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") + }) @PostMapping - public QuestionDto addQuestion(@Valid @RequestBody QuestionCreateDto dto) { + public ResponseEntity<QuestionDto> addQuestion(@Valid @RequestBody QuestionCreateDto dto) { try { - return questionFacade.create(dto); + QuestionDto createdQuestionDto = questionFacade.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(createdQuestionDto); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - e.getMessage()); + return ResponseEntity.notFound().build(); } } @@ -78,27 +117,52 @@ public class QuestionController { * Update question * * @param dto a QuestionDto object representing the updated question with correct id - * @return a QuestionUpdateDto object representing the updated question - * @throws ResponseStatusException if the question with id doesn't exist or its exercise doesn't exist + * @return a ResponseEntity containing a QuestionUpdateDto object representing the updated question, + * or a 404 Not Found response if the question with the specified ID was not found */ + @Operation(summary = "Update a question by ID", description = "Updates a question with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Question with the specified ID updated successfully."), + @ApiResponse(responseCode = "404", description = "Question with the specified ID was not found.") + }) @PutMapping("/{id}") - public QuestionDto updateQuestion(@NotBlank @PathVariable String id, @Valid @RequestBody QuestionUpdateDto dto) { + public ResponseEntity<QuestionDto> updateQuestion(@NotNull @PathVariable long id, @Valid @RequestBody QuestionUpdateDto dto) { try { - return questionFacade.update(id, dto); - } catch (IllegalArgumentException e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + return ResponseEntity.ok(questionFacade.patchUpdate(id, dto)); } catch (EntityNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + return ResponseEntity.notFound().build(); } } /** - * Add a new question to an exercise + * Delete question with ID from exercise * * @param id of question to delete */ + @Operation(summary = "Delete a question with specified ID", description = "Deletes a question with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Question with the specified ID deleted successfully."), + }) @DeleteMapping("/{id}") - public void deleteQuestion(@NotBlank @PathVariable String id) { + public ResponseEntity<Void> deleteQuestion(@NotNull @PathVariable long id) { questionFacade.delete(id); + return ResponseEntity.noContent().build(); + } + + /** + * Adds answers to the existing question resource + * + * @param id id of question to update + * @return the LectureDto representing the updated lecture + */ + @Operation(summary = "Add answers to the existing question.") + @PatchMapping("/{id}/answers") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The question has been successfully updated"), + @ApiResponse(responseCode = "400", description = "The request body is invalid"), + @ApiResponse(responseCode = "404", description = "The question with the specified ID does not exist") + }) + public ResponseEntity<QuestionDto> addAnswers(@PathVariable Long id, @RequestBody List<AnswerInQuestionCreateDto> answerDtoList) { + return ResponseEntity.ok(questionFacade.addAnswers(id, answerDtoList)); } } \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java index 2f2cfc9f4219b69c9c475ecc70c6c4de428233a5..8fe1e2a19f4fd2663aa3a22e0e5f9848829f8800 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java @@ -1,40 +1,36 @@ package org.fuseri.moduleexercise.question; -import org.fuseri.model.dto.common.Result; +import jakarta.transaction.Transactional; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; -import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.answer.AnswerMapper; import org.fuseri.moduleexercise.answer.AnswerService; -import org.fuseri.moduleexercise.exercise.Exercise; -import org.fuseri.moduleexercise.exercise.ExerciseService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; -import java.util.HashSet; import java.util.List; /** * Represent facade for managing questions * Provide simplified interface for manipulating with questions */ +@Transactional @Service public class QuestionFacade { private final QuestionService questionService; - private final ExerciseService exerciseService; private final AnswerService answerService; private final QuestionMapper questionMapper; private final AnswerMapper answerMapper; @Autowired public QuestionFacade( - QuestionService questionService, ExerciseService exerciseService, + QuestionService questionService, AnswerService answerService, QuestionMapper questionMapper, AnswerMapper answerMapper) { this.questionService = questionService; - this.exerciseService = exerciseService; this.answerService = answerService; this.questionMapper = questionMapper; this.answerMapper = answerMapper; @@ -46,21 +42,29 @@ public class QuestionFacade { * @param id the ID of the question to find * @return a QuestionDto object representing the found question */ - public QuestionDto find(String id) { - var a = questionService.find(id); - return questionMapper.toDto(a); + public QuestionDto find(long id) { + return questionMapper.toDto(questionService.find(id)); } /** - * Find questions by exercise ID and return them in a paginated format + * Retrieve a list of AnswerDto objects which belong to question with questionId * - * @param exerciseId the ID of the exercise to find questions for - * @param page the page number of the questions to retrieve - * @return a Result object containing a list of QuestionDto objects and pagination information + * @param questionId the ID of the question for which to retrieve answers + * @return a List of AnswerDto objects */ - public Result<QuestionDto> findByExerciseId(String exerciseId, int page) { - Page<Question> questions = questionService.findByExerciseId(exerciseId, page); - return questionMapper.toResult(questions); + public List<AnswerDto> getQuestionAnswers(long questionId) { + return answerMapper.toDtoList(answerService.findAllByQuestionId(questionId)); + } + + /** + * Add answers to question + * + * @param id question ID + * @param dto List with AnswerInQuestionCreateDto to add to question + * @return a QuestionDto object representing the updated question + */ + public QuestionDto addAnswers(Long id, List<AnswerInQuestionCreateDto> dto) { + return questionMapper.toDto(questionService.addAnswers(id, answerMapper.fromCreateDtoList(dto))); } /** @@ -70,30 +74,7 @@ public class QuestionFacade { * @return a QuestionDto object representing the added question */ public QuestionDto create(QuestionCreateDto dto) { - Question question = questionMapper.fromCreateDto(dto); - - Exercise exercise; - exercise = exerciseService.find(question.getExerciseId()); - - exercise.getQuestions().add(question); - question.setExerciseId(exercise.getId()); - - var answerDtos = dto.getAnswers(); - var answers = new HashSet<Answer>(); - for (var answerDto : answerDtos) { - Answer answer = answerMapper.fromCreateDto(answerDto); - answer = answerService.create(answer); - answers.add(answer); - } - - question.setAnswers(answers); - var createdQuestion = questionService.create(question); - - for (var answer : answers) { - answer.setQuestionId(createdQuestion.getId()); - } - - return questionMapper.toDto(createdQuestion); + return questionMapper.toDto(questionService.create(questionMapper.fromCreateDto(dto))); } /** @@ -102,12 +83,8 @@ public class QuestionFacade { * @param dto dto of updated question with correct id * @return dto of updated question */ - public QuestionDto update(String id, QuestionUpdateDto dto) { - Question question = questionMapper.fromUpdateDto(dto); - question.setId(id); - List<Answer> questionAnswers = answerService.findAllByQuestionId(id); - question.setAnswers(new HashSet<>(questionAnswers)); - Question updatedQuestion = questionService.update(question); + public QuestionDto patchUpdate(long id, QuestionUpdateDto dto) { + var updatedQuestion = questionService.patchUpdateWithoutAnswers(id, questionMapper.fromUpdateDto(dto)); return questionMapper.toDto(updatedQuestion); } @@ -116,11 +93,7 @@ public class QuestionFacade { * * @param id of qustion to delete */ - public void delete(String id) { - var question = questionService.find(id); - for (var answer : question.getAnswers()) { - answerService.delete(answer.getId()); - } + public void delete(long id) { questionService.delete(id); } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java index 9c4e62a2a7845c32941b219a539030fcdbf98a16..b3cd97079670ef1e2c239e08076a8748643b7110 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionMapper.java @@ -1,17 +1,53 @@ package org.fuseri.moduleexercise.question; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; +import org.fuseri.moduleexercise.answer.Answer; +import org.fuseri.moduleexercise.answer.AnswerMapper; import org.fuseri.moduleexercise.common.DomainMapper; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.exercise.ExerciseService; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Set; /** * Mapper between Questions and their corresponding DTOs */ -@Mapper -public interface QuestionMapper extends DomainMapper<Question, QuestionDto> { +@Mapper(componentModel = "spring", uses = {AnswerMapper.class, ExerciseService.class}) +public abstract class QuestionMapper implements DomainMapper<Question, QuestionDto> { + + @Autowired + private ExerciseService exerciseService; + + @Autowired + private AnswerMapper answerMapper; + + @Named("mapIdToExercise") + public Exercise mapIdToExercise(Long id) { + return exerciseService.find(id); + } + + @Named("mapDtoAnswersToAnswers") + public Set<Answer> mapDtoAnswersToAnswers(List<AnswerInQuestionCreateDto> dtos) { + return answerMapper.fromCreateDtoList(dtos); + } + + /** + * Convert entity to its corresponding DTO + * + * @param question to be converted + * @return corresponding DTO created from question + */ + @Override + @Mapping(target = "exerciseId", source = "question.exercise.id") + public abstract QuestionDto toDto(Question question); /** * Convert DTO of type QuestionCreateDto to Question @@ -19,8 +55,10 @@ public interface QuestionMapper extends DomainMapper<Question, QuestionDto> { * @param dto DTO to be converted * @return corresponding Question entity created from DTO */ - @Mapping(target = "answers", ignore = true) - Question fromCreateDto(QuestionCreateDto dto); + @Mapping(target = "exercise", source = "dto.exerciseId", qualifiedByName = "mapIdToExercise") + @Mapping(target = "answers", source = "dto.answers", qualifiedByName = "mapDtoAnswersToAnswers") + public abstract Question fromCreateDto(QuestionCreateDto dto); - Question fromUpdateDto(QuestionUpdateDto dto); + @Mapping(target = "exercise", source = "dto.exerciseId", qualifiedByName = "mapIdToExercise") + public abstract Question fromUpdateDto(QuestionUpdateDto dto); } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java index 0f924cd5794fbfb300a826813de050a1dc7b4222..b5b2adc4d3208451e1d1789654def49121feb14f 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepository.java @@ -1,20 +1,11 @@ package org.fuseri.moduleexercise.question; -import org.fuseri.moduleexercise.common.DomainRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; /** * A repository interface for managing Question entities */ -public interface QuestionRepository extends DomainRepository<Question, String> { - - /** - * Find a page of questions associated with the exercise with the specified ID - * - * @param exerciseId the ID of the exercise to find questions for - * @param pageRequest the page request specifying the page number and page size - * @return a page of questions associated with the specified exercise - */ - Page<Question> findByExerciseId(String exerciseId, PageRequest pageRequest); +@Repository +public interface QuestionRepository extends JpaRepository<Question, Long> { } \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java deleted file mode 100644 index 703b89dcd133afb330a3468fb0e24cc60d0e41d4..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionRepositoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.fuseri.moduleexercise.question; - -import org.fuseri.moduleexercise.common.DomainRepositoryImpl; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * An implementation of the QuestionRepository interface - * Provides access to Question entities stored in a data source - */ -@Repository -public class QuestionRepositoryImpl extends DomainRepositoryImpl<Question> implements QuestionRepository { - - /** - * Find a page of questions associated with the exercise with the specified ID - * - * @param exerciseId the ID of the exercise to find questions for - * @param pageRequest the page request specifying the page number and page size - * @return a page of questions associated with the specified exercise - */ - @Override - public Page<Question> findByExerciseId(String exerciseId, PageRequest pageRequest) { - List<Question> filteredQuestions = getItems() - .stream() - .filter(e -> e.getExerciseId().equals(exerciseId)) - .skip(pageRequest.getOffset()) - .limit(pageRequest.getPageSize()) - .toList(); - - return new PageImpl<>(filteredQuestions, pageRequest, filteredQuestions.size()); - } -} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java index f30867aa5da680b2f61004a645c8fe1b21534161..f0676eee28256d926e768b0fee5df3cf52275fac 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java @@ -2,13 +2,15 @@ package org.fuseri.moduleexercise.question; import jakarta.persistence.EntityNotFoundException; import lombok.Getter; +import org.fuseri.moduleexercise.answer.Answer; import org.fuseri.moduleexercise.common.DomainService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; +import java.util.Set; + /** * Represent a service for managing Question entities */ @@ -39,23 +41,48 @@ public class QuestionService extends DomainService<Question> { * @throws EntityNotFoundException if no Question entity exists with the specified ID */ @Transactional(readOnly = true) - public Question find(String id) { + public Question find(long id) { return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Question '" + id + "' not found.")); } /** - * Retrieve a page of Question entities associated with the specified exercise ID + * Patch update a question. Don't update question answers. * - * @param exerciseId the ID of the exercise to retrieve questions for - * @param page the page number to retrieve (0-indexed) - * @return a page of Question entities associated with the specified exercise ID + * @param id the question ID + * @param updatedQuestion the question to update + * @return the updated question */ - @Transactional(readOnly = true) - public Page<Question> findByExerciseId(String exerciseId, int page) { - return repository.findByExerciseId( - exerciseId, - PageRequest.of(page, DomainService.DEFAULT_PAGE_SIZE)); + @Transactional + public Question patchUpdateWithoutAnswers(Long id, Question updatedQuestion) { + Optional<Question> optionalQuestion = repository.findById(id); + if (optionalQuestion.isPresent()) { + Question question = optionalQuestion.get(); + question.setText(updatedQuestion.getText()); + question.setExercise(updatedQuestion.getExercise()); + return repository.save(question); + } else { + throw new EntityNotFoundException("Question with id: " + id + " was not found."); + } } + /** + * Add answers to question with question ID. + * + * @param id of question + * @param answers to add to question + * @return updated question + */ + public Question addAnswers(Long id, Set<Answer> answers) { + Optional<Question> optionalQuestion = repository.findById(id); + if (optionalQuestion.isPresent()) { + Question question = optionalQuestion.get(); + question.addAnswers(answers); + return repository.save(question); + } else { + throw new EntityNotFoundException( + "Question with id: " + id + " was not found."); + } + + } } diff --git a/application/module-exercise/src/main/resources/application.properties b/application/module-exercise/src/main/resources/application.properties index ec3c390e0877b6c499b7aade0abc0111d719d865..22a97362c389b6439b4b17bb0c4e7d144ce4f1a5 100644 --- a/application/module-exercise/src/main/resources/application.properties +++ b/application/module-exercise/src/main/resources/application.properties @@ -1 +1,3 @@ -server.port=5002 \ No newline at end of file +server.port=5002 +spring.h2.console.enabled=true +spring.datasource.url=jdbc:h2:mem:exercices \ No newline at end of file diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerControllerTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..66eaea5afcd6ba64f557df3e63151398ea0bf233 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerControllerTest.java @@ -0,0 +1,180 @@ +package org.fuseri.moduleexercise.answer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityNotFoundException; +import org.fuseri.model.dto.exercise.AnswerCreateDto; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; +import org.fuseri.model.dto.exercise.AnswersCreateDto; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.QuestionCreateDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class AnswerControllerTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AnswerFacade answerFacade; + long exerciseId = 1L; + ExerciseCreateDto exercise; + QuestionCreateDto question1; + QuestionCreateDto question2; + + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @BeforeEach + void init() { + exercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0); + question1 = new QuestionCreateDto("this statement is false", exerciseId, + List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); + question2 = new QuestionCreateDto("What month of the year has 28 days?", exerciseId, + List.of(new AnswerInQuestionCreateDto("February", false), + new AnswerInQuestionCreateDto("All of them", true))); + } + + @Test + void testCreateAnswer() throws Exception { + var answerCreateDto = new AnswerCreateDto("BA", true, 1); + var answerDto = new AnswerDto("BA", true); + when(answerFacade.create(ArgumentMatchers.isA(AnswerCreateDto.class))).thenReturn(answerDto); + + mockMvc.perform(post("/answers") + .content(asJsonString(answerCreateDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.text", is("BA"))) + .andExpect(jsonPath("$.correct", is(true))); + } + + @Test + void testCreateAnswerEmptyText() throws Exception { + var incorrect1 = new AnswerInQuestionCreateDto("", false); + var createAnswer = new AnswersCreateDto(1, List.of(incorrect1)); + + mockMvc.perform(post("/answers") + .content(asJsonString(createAnswer)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateAnswerMissingText() throws Exception { + var prompt = """ + { + "questionId": 1, + "answers": [ + { + "text": "something", + "correct": false + } + ] + } + """; + + mockMvc.perform(post("/answers") + .content(prompt) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateAnswerMissingCorrect() throws Exception { + + var prompt = """ + { + "questionId": 1, + "answers": [ + { + "text": "something" + } + ] + } + """; + + mockMvc.perform(post("/answers") + .content(prompt) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + void testUpdate() throws Exception { + long id = 1L; + var updated = new AnswerDto("dis true", false); + when(answerFacade.update(ArgumentMatchers.eq(id), ArgumentMatchers.isA(AnswerCreateDto.class))).thenReturn(updated); + + mockMvc.perform(put("/answers/{id}", id) + .content(asJsonString(updated)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.text", is("dis true"))) + .andExpect(jsonPath("$.correct", is(false))); + } + + @Test + void testUpdateNotFoundAnswer() throws Exception { + long id = 1L; + var toUpdate = new AnswerCreateDto("dis true", false, 1); + when(answerFacade.update(ArgumentMatchers.eq(id), ArgumentMatchers.isA(AnswerCreateDto.class))).thenThrow(new EntityNotFoundException()); + mockMvc.perform(put("/answers/{id}", id) + .content(asJsonString(toUpdate)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testUpdateEmptyText() throws Exception { + var updated = new AnswerCreateDto("", false, 1); + mockMvc.perform(put("/answers/1") + .content(asJsonString(updated)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + void testUpdateMissingField() throws Exception { + var updated = """ + { + "correct": false, + "questionId": 1 + } + """; + + mockMvc.perform(put("/answers/1") + .content(updated).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete("/answers/1")) + .andExpect(status().is2xxSuccessful()); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerFacadeTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f0faeebafdfaa1d1ec4986277f83495f879ef7b0 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerFacadeTest.java @@ -0,0 +1,53 @@ +package org.fuseri.moduleexercise.answer; + +import org.fuseri.model.dto.exercise.AnswerCreateDto; +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.question.Question; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +class AnswerFacadeTest { + + @Autowired + private AnswerFacade answerFacade; + + @MockBean + private AnswerService answerService; + + @MockBean + private AnswerMapper answerMapper; + + private final AnswerDto answerDto = new AnswerDto("name", true); + private final AnswerCreateDto answerCreateDto = new AnswerCreateDto("name", true, 1L); + private final Question question = new Question("text", new HashSet<>(), new Exercise()); + private final Answer answer = new Answer("name", true, question); + + @Test + void update() { + long id = 1L; + when(answerMapper.fromCreateDto(answerCreateDto)).thenReturn(answer); + when(answerService.update(id, answer)).thenReturn(answer); + when(answerMapper.toDto(answer)).thenReturn(answerDto); + + AnswerDto actualDto = answerFacade.update(id, answerCreateDto); + assertEquals(answerDto, actualDto); + } + + @Test + void delete() { + long id = 1L; + answerFacade.delete(id); + verify(answerService).delete(id); + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerRepositoryTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0f567b69064c57addccf48a92a66c5c5c72f3896 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerRepositoryTest.java @@ -0,0 +1,91 @@ +package org.fuseri.moduleexercise.answer; + +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.question.Question; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +@DataJpaTest +class AnswerRepositoryTest { + + @Autowired + AnswerRepository answerRepository; + + @Autowired + TestEntityManager entityManager; + + Exercise exercise = new Exercise("name", "desc", 2, 1L, new HashSet<>()); + + Question question = new Question("text", new HashSet<>(), exercise); + + Answer answer = new Answer("text", false, question); + Answer answer2 = new Answer("text2", true, question); + + @BeforeEach + void init() { + entityManager.persist(exercise); + entityManager.persist(question); + } + + @Test + void saveAnswer() { + Answer createdAnswer = answerRepository.save(answer); + + Assertions.assertNotNull(createdAnswer); + Assertions.assertEquals(answer, createdAnswer); + } + + @Test + void findByQuestionId() { + entityManager.persist(answer); + entityManager.flush(); + + Answer foundAnswer = answerRepository.findById(answer.getId()).orElse(null); + + Assertions.assertNotNull(foundAnswer); + Assertions.assertEquals(foundAnswer, answer); + } + + @Test + void testFindAllQuestions() { + entityManager.persist(exercise); + entityManager.persist(question); + entityManager.persist(answer); + entityManager.persist(answer2); + entityManager.flush(); + + Page<Answer> coursePage = answerRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), Arrays.asList(answer, answer2)); + } + + @Test + void testFindAllQuestionsEmpty() { + Page<Answer> coursePage = answerRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(0, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), new ArrayList<>()); + } + + @Test + void testDeleteAnswer() { + Long id = entityManager.persist(answer).getId(); + entityManager.flush(); + + answerRepository.deleteById(id); + + Assertions.assertTrue(answerRepository.findById(id).isEmpty()); + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ee53178ff5e7f13e73f3ed97da52421e3c6e3dcb --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java @@ -0,0 +1,78 @@ +package org.fuseri.moduleexercise.answer; + +import jakarta.persistence.EntityNotFoundException; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.question.Question; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +class AnswerServiceTest { + + @Autowired + AnswerService service; + + @MockBean + AnswerRepository answerRepository; + + Answer answer; + Question question; + Exercise exercise; + + @BeforeEach + void setup() { + exercise = new Exercise("idioms", "exercise on basic idioms", 2, 1L, new HashSet<>()); + question = new Question("text", new HashSet<>(), exercise); + answer = new Answer("text", false, question); + } + + + @Test + void create() { + when(answerRepository.save(answer)).thenReturn(answer); + Answer result = service.create(answer); + Assertions.assertEquals(answer, result); + verify(answerRepository).save(answer); + } + + @Test + void notFound() { + when(answerRepository.findById(anyLong())).thenReturn(Optional.empty()); + Assertions.assertThrows(EntityNotFoundException.class, () -> service.find(anyLong())); + } + + @Test + void find() { + when(answerRepository.findById(anyLong())).thenReturn(Optional.of(answer)); + + Answer result = service.find(anyLong()); + + Assertions.assertEquals(answer, result); + verify(answerRepository).findById(anyLong()); + } + + @Test + void findAllByQuestionId() { + long id = 1; + List<Answer> list = Collections.emptyList(); + + when(answerRepository.existsById(id)).thenReturn(true); + when(answerRepository.findByQuestionId(id)).thenReturn(list); + List<Answer> result = service.findAllByQuestionId(1L); + + Assertions.assertEquals(list, result); + verify(answerRepository).findByQuestionId(id); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java deleted file mode 100644 index da00608e87053cae0b248e864e0a5797d816f9bc..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.fuseri.moduleexercise.answer; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.fuseri.model.dto.exercise.AnswerDto; -import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; -import org.fuseri.model.dto.exercise.AnswersCreateDto; -import org.fuseri.model.dto.exercise.ExerciseCreateDto; -import org.fuseri.model.dto.exercise.ExerciseDto; -import org.fuseri.model.dto.exercise.QuestionCreateDto; -import org.fuseri.model.dto.exercise.QuestionDto; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import java.util.List; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; - -@SpringBootTest -@AutoConfigureMockMvc -public class AnswerTest { - - @Autowired - ObjectMapper objectMapper; - @Autowired - private MockMvc mockMvc; - - public static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private QuestionDto createQuestion(String id) throws Exception { - var question = new QuestionCreateDto("this statement is false", id, - List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); - - - var posted = mockMvc.perform(post("/questions") - .content(asJsonString(question)) - .contentType(MediaType.APPLICATION_JSON)); - - var cont = posted.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(cont, QuestionDto.class); - return res; - } - - - private String createExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - - String id = ""; - - try { - var dis = mockMvc.perform(post("/exercises") - .content(asJsonString(postExercise)) - .contentType(MediaType.APPLICATION_JSON)); - var ok = dis.andReturn().getResponse().getContentAsString(); - - var ll = objectMapper.readValue(ok, ExerciseDto.class); - - id = ll.getId(); - } catch (Exception e) { - assert (false); - } - return id; - } - - - @Test - void testCreateAnswer() throws Exception { - - List<AnswerDto> res = createAnswer(); - - var expected1 = new AnswerDto("True", false); - var expected2 = new AnswerDto("False", false); - - assert (res.get(0).equals(expected1)); - assert (res.get(1).equals(expected2)); - - } - - private List<AnswerDto> createAnswer() throws Exception { - var exerciseId = createExercise(); - var question = createQuestion(exerciseId); - - var incorrect1 = new AnswerInQuestionCreateDto("True", false); - var incorrect2 = new AnswerInQuestionCreateDto("False", false); - - var createAnswer = new AnswersCreateDto(question.getId(), List.of(incorrect1, incorrect2)); - - var posted = mockMvc.perform(post("/answers") - .content(asJsonString(createAnswer)) - .contentType(MediaType.APPLICATION_JSON)); - - var asStr = posted.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(asStr, new TypeReference<List<AnswerDto>>() { - }); - return res; - } - - - @Test - void getAnswer() throws Exception { - var exerciseId = createExercise(); - var question = createQuestion(exerciseId); - - var gets = mockMvc.perform(get(String.format("/answers/%s", question.getId()))); - - var content2 = gets.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(content2, new TypeReference<List<AnswerDto>>() { - }); - - assert (res.equals(question.getAnswers())); - - } - - @Test - void testUpdate() throws Exception { - - var exerciseId = createExercise(); - var question = createQuestion(exerciseId); - - var incorrect1 = new AnswerInQuestionCreateDto("True", false); - var incorrect2 = new AnswerInQuestionCreateDto("False", false); - - - var createAnswer = new AnswersCreateDto(question.getId(), List.of(incorrect1, incorrect2)); - - - var posted = mockMvc.perform(post("/answers") - .content(asJsonString(createAnswer)) - .contentType(MediaType.APPLICATION_JSON)); - - var asStr = posted.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(asStr, new TypeReference<List<AnswerDto>>() { - }); - - - var updated = """ - { - "text": "dis true", - "correct": false, - "questionId": "%s" - } - """; - - - updated = String.format(updated, question.getId()); - - var puts = mockMvc.perform(put(String.format("/answers/%s", res.get(0).getId())) - .content(updated).contentType(MediaType.APPLICATION_JSON)); - - var content = puts.andReturn().getResponse().getContentAsString(); - - var res2 = objectMapper.readValue(content, AnswerDto.class); - - var expected = new AnswerDto("dis true", false); - - assert res2.equals(expected); - - } - -} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseControllerTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dce5d42843d4279b7813b5a9aa65038c41d245ff --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseControllerTest.java @@ -0,0 +1,261 @@ +package org.fuseri.moduleexercise.exercise; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityNotFoundException; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@AutoConfigureMockMvc +public class ExerciseControllerTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @MockBean + ExerciseFacade facade; + + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ExerciseDto exerciseDto; + private ExerciseDto exerciseDto1; + private ExerciseDto exerciseDto2; + private ExerciseCreateDto exerciseCreateDto; + private ExerciseCreateDto exerciseCreateDto1; + private ExerciseCreateDto exerciseCreateDto2; + + + @BeforeEach + void init() { + exerciseDto = new ExerciseDto(); + exerciseDto.setName("idioms"); + exerciseDto.setDescription("exercise on basic idioms"); + exerciseDto.setDifficulty(2); + exerciseDto.setCourseId(0); + + exerciseDto1 = new ExerciseDto(); + exerciseDto1.setName("idioms1"); + exerciseDto1.setDescription("exercise on basic idioms"); + exerciseDto1.setDifficulty(2); + exerciseDto1.setCourseId(0); + + exerciseDto2 = new ExerciseDto(); + exerciseDto2.setName("idioms2"); + exerciseDto2.setDescription("exercise on basic idioms"); + exerciseDto2.setDifficulty(1); + exerciseDto2.setCourseId(0); + + exerciseCreateDto = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0); + exerciseCreateDto1 = new ExerciseCreateDto("idioms1", "exercise on intermediate idioms", 2, 0); + exerciseCreateDto2 = new ExerciseCreateDto("idioms2", "exercise on basic idioms", 1, 0L); + } + + @Test + void getExercise() throws Exception { + long id = 1L; + when(facade.find(id)).thenReturn(exerciseDto); + mockMvc.perform(get("/exercises/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(exerciseCreateDto.getName()))) + .andExpect(jsonPath("$.description", is(exerciseCreateDto.getDescription()))) + .andExpect(jsonPath("$.courseId", is((int) exerciseCreateDto.getCourseId()))) + .andExpect(jsonPath("$.difficulty", is(exerciseCreateDto.getDifficulty()))); + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete("/exercises/1")) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getExercise_notFound() throws Exception { + long id = 1L; + when(facade.find(id)).thenThrow(new EntityNotFoundException("")); + mockMvc.perform(get("/exercises/{id}", id)) + .andExpect(status().isNotFound()); + } + + @Test + void FindAll() { + when(facade.findAll(0)).thenReturn(new PageImpl<>(new ArrayList<>())); + try { + var response = mockMvc.perform(get("/exercises").param("page", "0")); + + var dis = response.andExpect(status().isOk()) + .andExpect(status().is2xxSuccessful()).andReturn().getResponse().getContentAsString(); + assertTrue(dis.contains("\"content\":[]")); + } catch (Exception e) { + throw new RuntimeException(e); + } + + + } + + @Test + void getFiltered() { + when(facade.findByCourseIdAndDifficulty(0, 2, 0)).thenReturn(new PageImpl<>(new ArrayList<>())); + try { + var filtered = mockMvc.perform(get("/exercises/filter").param("page", "0").param("courseId", "0").param("difficulty", "2")); + var content = filtered.andReturn().getResponse().getContentAsString(); + + assertTrue(content.contains("\"content\":[]")); + } catch (Exception e) { + assert (false); + } + } + + @Test + void testCreateExercise() throws Exception { + when(facade.create(ArgumentMatchers.isA(ExerciseCreateDto.class))).thenReturn(exerciseDto); + mockMvc.perform(post("/exercises").content(asJsonString(exerciseCreateDto)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("idioms")) + .andExpect(jsonPath("$.description").value("exercise on basic idioms")) + .andExpect(jsonPath("$.difficulty").value(2)) + .andExpect(jsonPath("$.courseId").value("0")).andReturn().getResponse().getContentAsString(); + } + + @Test + void testCreateExerciseEmptyBody() throws Exception { + var postExercise = ""; + mockMvc.perform(post("/exercises").content((postExercise)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()).andReturn().getResponse().getContentAsString(); + } + + + @Test + void testCreateExerciseMissingDesc() throws Exception { + var postExercise = """ + { + "name": "idioms", + "difficulty": 2, + "courseId": 0, + } + """; + + mockMvc.perform(post("/exercises").content((postExercise)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()).andReturn().getResponse().getContentAsString(); + } + + @Test + void testCreateExerciseMissingName() throws Exception { + var postExercise = """ + { + "description: "exercise on basic idioms", + "difficulty": 2, + "courseId": 0, + } + """; + + mockMvc.perform(post("/exercises").content((postExercise)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()).andReturn().getResponse().getContentAsString(); + } + + @Test + void testCreateExerciseMissingDifficulty() throws Exception { + var postExercise = """ + { + "name": "idioms" + "description: "exercise on basic idioms", + "courseId": 0 + } + """; + + mockMvc.perform(post("/exercises").content((postExercise)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()).andReturn().getResponse().getContentAsString(); + } + + @Test + void testCreateExerciseMissingCourseId() throws Exception { + var postExercise = """ + { + "name": "idioms" + "description: "exercise on basic idioms", + "difficulty": 0 + } + """; + + mockMvc.perform(post("/exercises").content((postExercise)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()).andReturn().getResponse().getContentAsString(); + } + + + @Test + void testUpdate() throws Exception { + long id = 1L; + when(facade.update(ArgumentMatchers.eq(id), ArgumentMatchers.isA(ExerciseCreateDto.class))).thenReturn(exerciseDto); + + var theId = String.format("/exercises/%d", id); + var dis = mockMvc.perform(put(theId).content(asJsonString(exerciseCreateDto)).contentType(MediaType.APPLICATION_JSON)); + + dis.andExpect(status().isOk()).andExpect(jsonPath("$.name", is(exerciseDto.getName()))) + .andExpect(jsonPath("$.difficulty", is(exerciseDto.getDifficulty()))) + .andExpect(jsonPath("$.description", is(exerciseDto.getDescription()))); + } + + @Test + void testUpdateNotFound() { + long id = 9999L; + when(facade.update(ArgumentMatchers.eq(id), ArgumentMatchers.isA(ExerciseCreateDto.class))).thenThrow(new EntityNotFoundException()); + + try { + var theId = String.format("/exercises/%d", id); + mockMvc.perform(put(theId).content(asJsonString(exerciseCreateDto)).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + void testUpdateIncorrectBody() { + long id = 1L; + var content = """ + { + "description": "exercise on basic idioms", + "difficulty": 2, + "courseId": 0 + } + """; + try { + var theId = String.format("/exercises/%d", id); + mockMvc.perform(put(theId).content(content).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseFacadeTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0be6f3770b15f6963a2a3200e888efd8115cd4c3 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseFacadeTest.java @@ -0,0 +1,99 @@ +package org.fuseri.moduleexercise.exercise; + +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.model.dto.exercise.ExerciseDto; +import org.junit.jupiter.api.Test; +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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +@AutoConfigureMockMvc +class ExerciseFacadeTest { + + @Autowired + ExerciseFacade exerciseFacade; + + @MockBean + ExerciseService exerciseService; + + @MockBean + ExerciseMapper exerciseMapper; + + private final ExerciseDto exerciseDto = new ExerciseDto(); + + private final ExerciseCreateDto exerciseCreateDto = new ExerciseCreateDto("name", "desc", 2, 1); + + private final Exercise exercise = new Exercise(); + + @Test + void create() { + when(exerciseMapper.fromCreateDto(exerciseCreateDto)).thenReturn(exercise); + when(exerciseService.create(exercise)).thenReturn(exercise); + when(exerciseMapper.toDto(exercise)).thenReturn(exerciseDto); + + ExerciseDto actualDto = exerciseFacade.create(exerciseCreateDto); + + assertEquals(exerciseDto, actualDto); + } + + @Test + void testFindById() { + long id = 1L; + + when(exerciseService.find(id)).thenReturn(exercise); + when(exerciseMapper.toDto(exercise)).thenReturn(exerciseDto); + + ExerciseDto actualDto = exerciseFacade.find(id); + + assertNotNull(actualDto); + assertEquals(exerciseDto, actualDto); + } + + @Test + void testFindAll() { + int page = 0; + Pageable pageable = PageRequest.of(0, 10); + Page<Exercise> exercisePage = new PageImpl<>(List.of(exercise), pageable, 0); + Page<ExerciseDto> expectedPageDto = new PageImpl<>(List.of(exerciseDto)); + + when(exerciseService.findAll(page)).thenReturn(exercisePage); + when(exerciseMapper.toDtoPage(exercisePage)).thenReturn(expectedPageDto); + + Page<ExerciseDto> actualPageDto = exerciseFacade.findAll(page); + + assertEquals(expectedPageDto, actualPageDto); + } + + @Test + void update() { + long id = 1L; + when(exerciseMapper.fromCreateDto(exerciseCreateDto)).thenReturn(exercise); + when(exerciseService.update(id, exercise)).thenReturn(exercise); + when(exerciseMapper.toDto(exercise)).thenReturn(exerciseDto); + + ExerciseDto actualDto = exerciseFacade.update(id, exerciseCreateDto); + + assertEquals(exerciseDto, actualDto); + } + + @Test + void testDelete() { + long id = 1L; + exerciseFacade.delete(id); + verify(exerciseService).delete(id); + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1d8eec54a05e1384cfed5cb5aa136f5c680410ce --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseRepositoryTest.java @@ -0,0 +1,80 @@ +package org.fuseri.moduleexercise.exercise; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.Arrays; +import java.util.HashSet; + +@DataJpaTest +class ExerciseRepositoryTest { + + @Autowired + ExerciseRepository exerciseRepository; + + @Autowired + TestEntityManager entityManager; + + Exercise exercise = new Exercise("name", "desc", 2, 1L, new HashSet<>()); + + @Test + void saveExercise() { + Exercise saved = exerciseRepository.save(exercise); + + Assertions.assertNotNull(saved); + Assertions.assertEquals(exercise, saved); + } + + @Test + void findById() { + entityManager.persist(exercise); + entityManager.flush(); + + Exercise found = exerciseRepository.findById(exercise.getId()).orElse(null); + + Assertions.assertNotNull(found); + Assertions.assertEquals(found, exercise); + } + + + @Test + void findByCourseIdAndDifficulty() { + entityManager.persist(exercise); + entityManager.flush(); + + Page<Exercise> found = exerciseRepository.findByCourseIdAndDifficulty(1L, 2, PageRequest.of(0, 10)); + + Assertions.assertEquals(1, found.getTotalElements()); + Assertions.assertEquals(found.getContent().get(0), exercise); + } + + @Test + void testFindAllExercises() { + Exercise exercise1 = new Exercise(); + + entityManager.persist(exercise); + entityManager.persist(exercise1); + entityManager.flush(); + + Page<Exercise> coursePage = exerciseRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), Arrays.asList(exercise, exercise1)); + } + + @Test + void testDeleteExercise() { + Long id = entityManager.persist(exercise).getId(); + entityManager.flush(); + + exerciseRepository.deleteById(id); + + Assertions.assertTrue(exerciseRepository.findById(id).isEmpty()); + } + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseServiceTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7d54a6cc2869ee3e00af9abb67933de0ca71a9c6 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseServiceTest.java @@ -0,0 +1,111 @@ +package org.fuseri.moduleexercise.exercise; + +import jakarta.persistence.EntityNotFoundException; +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.fuseri.moduleexercise.question.Question; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SpringBootTest +class ExerciseServiceTest { + + @MockBean + ExerciseRepository exerciseRepository; + + @Autowired + ExerciseService exerciseService; + + Exercise exercise; + ExerciseCreateDto exercise2; + ExerciseCreateDto exercise3; + + @BeforeEach + void setup() { + exercise = new Exercise("idioms", "exercise on basic idioms", 2, 1L, new HashSet<>()); + exercise2 = new ExerciseCreateDto("idioms1", "exercise on intermediate idioms", 2, 0); + exercise3 = new ExerciseCreateDto("idioms2", "exercise on basic idioms", 1, 0L); + } + + @Test + void create() { + when(exerciseRepository.save(exercise)).thenReturn(exercise); + Exercise result = exerciseService.create(exercise); + Assertions.assertEquals(exercise, result); + verify(exerciseRepository).save(exercise); + } + + @Test + void notFound() { + when(exerciseRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(EntityNotFoundException.class, () -> exerciseService.find(anyLong())); + } + + @Test + void find() { + when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); + + Exercise result = exerciseService.find(anyLong()); + + Assertions.assertEquals(exercise, result); + verify(exerciseRepository).findById(anyLong()); + } + + @Test + void findAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<Exercise> page = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(exerciseRepository.findAll(pageable)).thenReturn(page); + Page<Exercise> result = exerciseService.findAll(0); + + Assertions.assertEquals(page, result); + verify(exerciseRepository).findAll(pageable); + } + + @Test + void findByCourseIdAndDifficulty() { + PageRequest pageable = PageRequest.of(0, 10); + Page<Exercise> page = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(exerciseRepository.findByCourseIdAndDifficulty(1L, 2, pageable)).thenReturn(page); + + Page<Exercise> result = exerciseService.findByCourseIdAndDifficulty(1L, 2, 0); + + Assertions.assertEquals(page, result); + verify(exerciseRepository).findByCourseIdAndDifficulty(1L, 2, pageable); + } + + + @Test + void getQuestions() { + PageRequest pageable = PageRequest.of(0, 10); + Page<Question> page = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(exerciseRepository.getQuestions(pageable, 1L)).thenReturn(page); + when(exerciseRepository.existsById(1L)).thenReturn(true); + + + Page<Question> result = exerciseService.getQuestions(1L, 0); + + Assertions.assertEquals(page, result); + verify(exerciseRepository).getQuestions(pageable, 1L); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java deleted file mode 100644 index a634b41941ff09a1d6fa1a82125c2015c3206bf1..0000000000000000000000000000000000000000 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.fuseri.moduleexercise.exercise; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.fuseri.model.dto.common.Result; -import org.fuseri.model.dto.exercise.ExerciseCreateDto; -import org.fuseri.model.dto.exercise.ExerciseDto; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Map; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; - - -@SpringBootTest -@AutoConfigureMockMvc -public class ExerciseTest { - @Autowired - ObjectMapper objectMapper; - @Autowired - private MockMvc mockMvc; - - public static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - @Test - void getExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - - String id = ""; - - try { - var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); - var ok = dis.andReturn().getResponse().getContentAsString(); - - var ll = objectMapper.readValue(ok, ExerciseDto.class); - - id = ll.getId(); - } catch (Exception e) { - assert (false); - } - - - try { - var theId = String.format("/exercises/%s", id); - var smth = mockMvc.perform(get(theId)); - - } catch (Exception e) { - //do absolutely nothing - } - } - - @Test - void getFiltered() { - - - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 0, "0"); - var postExercise1 = new ExerciseCreateDto("idioms1", "exercise on basic idioms", 0, "0"); - var postExercise2 = new ExerciseCreateDto("idioms2", "exercise on basic idioms", 1, "0"); - - try { - var exercise1 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); - - var exercise2 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise1)).contentType(MediaType.APPLICATION_JSON)); - var exercise3 = mockMvc.perform(post("/exercises").content(asJsonString(postExercise2)).contentType(MediaType.APPLICATION_JSON)); - } catch (Exception e) { - //do absolutly nothing - } - - - Map<String, String> params; - - try { - var filtered = mockMvc.perform(get("/exercises/filter").param("page", "0").param("courseId", "0").param("difficulty", "0")); - - var content = filtered.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(content, new TypeReference<Result<ExerciseDto>>() { - }); - - assert (res.getTotal() == 2); - } catch (Exception e) { - assert (false); - } - } - - @Test - void testCreateExercise() throws Exception { - var expectedResponse = new ExerciseDto(); - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - - mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("idioms")).andExpect(jsonPath("$.description").value("exercise on basic idioms")).andExpect(jsonPath("$.difficulty").value(2)).andExpect(jsonPath("$.courseId").value("0")).andReturn().getResponse().getContentAsString(); - } - - @Test - void testUpdate() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - - String id = ""; - - try { - var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); - var ok = dis.andReturn().getResponse().getContentAsString(); - - var ll = objectMapper.readValue(ok, ExerciseDto.class); - - id = ll.getId(); - } catch (Exception e) { - assert (false); - } - - var expectedExercise = new ExerciseDto(); - expectedExercise.setId(id); - expectedExercise.setName("idioms"); - expectedExercise.setDifficulty(2); - expectedExercise.setCourseId("idioms"); - expectedExercise.setDescription("exercise on basic idioms"); - - var content = """ - { - "name": "idioms", - "description": "exercise on basic idioms", - "difficulty": 2, - "courseId": "idioms" - } - """; - - - try { - var theId = String.format("/exercises/%s", id); - var dis = mockMvc.perform(put(theId).content(content).contentType(MediaType.APPLICATION_JSON)); - - var str = dis.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(str, ExerciseDto.class); - - assert res.equals(expectedExercise); - - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - -} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..33e1ca0a8f8b9ff86d3ec16f12a5df86738c5d29 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java @@ -0,0 +1,125 @@ +package org.fuseri.moduleexercise.question; + + +import org.fuseri.model.dto.exercise.AnswerDto; +import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; +import org.fuseri.model.dto.exercise.QuestionCreateDto; +import org.fuseri.model.dto.exercise.QuestionDto; +import org.fuseri.model.dto.exercise.QuestionUpdateDto; +import org.fuseri.moduleexercise.answer.Answer; +import org.fuseri.moduleexercise.answer.AnswerMapper; +import org.fuseri.moduleexercise.answer.AnswerService; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +class QuestionFacadeTest { + + @Autowired + QuestionFacade questionFacade; + + @MockBean + QuestionService questionService; + + @MockBean + AnswerService answerService; + + @MockBean + QuestionMapper questionMapper; + + @MockBean + AnswerMapper answerMapper; + + private final QuestionDto questionDto = new QuestionDto(); + + private final QuestionCreateDto questionCreateDto = new QuestionCreateDto("name", 1L, new ArrayList<>()); + + private final QuestionUpdateDto questionUpdateDto = QuestionUpdateDto.builder().build(); + + private final Exercise exercise = new Exercise(); + + private final Question question = new Question("text", new HashSet<>(), exercise); + Set<Answer> answers = new HashSet<>(List.of(new Answer("BA", true, question))); + + @Test + void create() { + when(questionMapper.fromCreateDto(questionCreateDto)).thenReturn(question); + when(questionService.create(question)).thenReturn(question); + when(questionMapper.toDto(question)).thenReturn(questionDto); + + QuestionDto actualDto = questionFacade.create(questionCreateDto); + + assertEquals(questionDto, actualDto); + } + + @Test + void find() { + long id = 1L; + + when(questionService.find(id)).thenReturn(question); + when(questionMapper.toDto(question)).thenReturn(questionDto); + + QuestionDto actualDto = questionFacade.find(id); + + assertNotNull(actualDto); + assertEquals(questionDto, actualDto); + } + + @Test + void patchUpdate() { + long id = 1L; + when(questionMapper.fromUpdateDto(questionUpdateDto)).thenReturn(question); + when(questionService.patchUpdateWithoutAnswers(id, question)).thenReturn(question); + when(questionMapper.toDto(question)).thenReturn(questionDto); + QuestionDto actualDto = questionFacade.patchUpdate(id, questionUpdateDto); + + assertEquals(questionDto, actualDto); + } + + @Test + void testDelete() { + long id = 1L; + when(questionService.find(id)).thenReturn(question); + + questionFacade.delete(id); + verify(questionService).delete(id); + } + + @Test + void getQuestionAnswers() { + long id = 1L; + List<Answer> answers = List.of(new Answer("BA", true, question)); + List<AnswerDto> answerDtos = List.of(new AnswerDto("BA", true)); + when(answerMapper.toDtoList(answers)).thenReturn(answerDtos); + when(answerService.findAllByQuestionId(id)).thenReturn(answers); + var actualAnswers = questionFacade.getQuestionAnswers(id); + assertEquals(answerDtos, actualAnswers); + } + + @Test + void addAnswers() { + long id = 1L; + question.setAnswers(answers); + List<AnswerInQuestionCreateDto> answerDtos = List.of(new AnswerInQuestionCreateDto("BA", true)); + QuestionDto questionDtoAnswers = new QuestionDto(question.getText(), question.getExercise().getId(), List.of(new AnswerDto("BA", true))); + + when(answerMapper.fromCreateDtoList(answerDtos)).thenReturn(answers); + when(questionService.addAnswers(id, answers)).thenReturn(question); + when(questionMapper.toDto(question)).thenReturn(questionDtoAnswers); + QuestionDto actualQuestionDto = questionFacade.addAnswers(id, answerDtos); + assertEquals(questionDtoAnswers, actualQuestionDto); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionRepositoryTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3c0eb33e9b5b83677709c5d43c1f8c47d94db32d --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionRepositoryTest.java @@ -0,0 +1,88 @@ +package org.fuseri.moduleexercise.question; + +import org.fuseri.moduleexercise.exercise.Exercise; +import org.fuseri.moduleexercise.exercise.ExerciseRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +@DataJpaTest +public class QuestionRepositoryTest { + + @Autowired + QuestionRepository repository; + + @Autowired + ExerciseRepository exerciseRepository; + + @Autowired + TestEntityManager entityManager; + + + Exercise exercise = new Exercise("name","desc",2,1L,new HashSet<>()); + + Question question = new Question("text",new HashSet<>(),exercise); + Question question2 = new Question("text2",new HashSet<>(),exercise); + + @BeforeEach + void init() { + exerciseRepository.save(exercise); + } + + @Test + void saveQuestion() { + Question saved = repository.save(question); + + Assertions.assertNotNull(saved); + Assertions.assertEquals(question, saved); + } + + @Test + void findById() { + entityManager.persist(question); + entityManager.flush(); + + Question found = repository.findById(question.getId()).orElse(null); + + Assertions.assertNotNull(found); + Assertions.assertEquals(found, question); + } + + + @Test + void testFindAllQuestions() { + entityManager.persist(question); + entityManager.persist(question2); + + Page<Question> coursePage = repository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), Arrays.asList(question, question2)); + } + @Test + void testFindAllQuestionsEmpty() { + Page<Question> coursePage = repository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(0, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), new ArrayList<>()); + } + + @Test + void testDeleteQuestion() { + Long id = entityManager.persist(question).getId(); + entityManager.flush(); + + repository.deleteById(id); + + Assertions.assertTrue(repository.findById(id).isEmpty()); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionServiceTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..298510e1ea93958b62dc321e70031f2c8d0c2ab3 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionServiceTest.java @@ -0,0 +1,83 @@ +package org.fuseri.moduleexercise.question; + +import jakarta.persistence.EntityNotFoundException; +import org.fuseri.moduleexercise.answer.Answer; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +class QuestionServiceTest { + + @MockBean + QuestionRepository questionRepository; + + @Autowired + QuestionService questionService; + + Exercise exercise; + Question question; + Question question1; + + @BeforeEach + void setup() { + exercise = new Exercise("idioms", "exercise on basic idioms",2,1L,new HashSet<>()); + question = new Question("text",new HashSet<>(),exercise); + question1 = new Question("text2",new HashSet<>(),exercise); + } + + + @Test + void create() { + when(questionRepository.save(question)).thenReturn(question); + Question result = questionService.create(question); + Assertions.assertEquals(question, result); + verify(questionRepository).save(question); + } + + @Test + void notFound() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(EntityNotFoundException.class, () -> questionService.find(anyLong())); + } + + @Test + void find() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.of(question)); + + Question result = questionService.find(anyLong()); + + Assertions.assertEquals(question, result); + verify(questionRepository).findById(anyLong()); + } + + @Test + void patchUpdateWithoutAnswers() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.of(question)); + when(questionRepository.save(question)).thenReturn(question); + Question result = questionService.patchUpdateWithoutAnswers(anyLong(), question); + Assertions.assertEquals(question, result); + } + + @Test + void addAnswers() { + Set<Answer> answers = Set.of(new Answer("BA", true, question)); + question.setAnswers(answers); + when(questionRepository.findById(anyLong())).thenReturn(Optional.of(question)); + when(questionRepository.save(question)).thenReturn(question); + Question result = questionService.patchUpdateWithoutAnswers(anyLong(), question); + Assertions.assertEquals(question, result); + } +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java index a099ff7c8cf058e033931222d5c76469ac40003e..8383971d23f186ac9740263ebb47ed4ab303be1f 100644 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java @@ -1,27 +1,41 @@ package org.fuseri.moduleexercise.question; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import org.fuseri.model.dto.common.Result; +import jakarta.persistence.EntityNotFoundException; import org.fuseri.model.dto.exercise.AnswerDto; import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; import org.fuseri.model.dto.exercise.ExerciseCreateDto; -import org.fuseri.model.dto.exercise.ExerciseDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; +import org.fuseri.model.dto.exercise.QuestionUpdateDto; +import org.fuseri.moduleexercise.exercise.Exercise; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; 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.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @SpringBootTest @@ -33,6 +47,11 @@ public class QuestionTest { @Autowired private MockMvc mockMvc; + @MockBean + QuestionFacade facade; + + private QuestionDto qston; + public static String asJsonString(final Object obj) { try { return new ObjectMapper().writeValueAsString(obj); @@ -41,113 +60,146 @@ public class QuestionTest { } } - @Test - void testCreateQuestion() throws Exception { - String id = createExercise(); - var answr = new AnswerDto("dis a logical paradox", true); - QuestionDto res = createQuestion(id); - var expected = new QuestionDto(); - expected.setAnswers(List.of(answr)); - expected.setExerciseId(id); - expected.setId(res.getId()); - expected.setText("this statement is false"); - - assert expected.equals(res); + private void createExercise() { + var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0L); + var postExercise2 = new ExerciseCreateDto("riddles", "simple english riddles", 2, 0L); + try { + mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); + mockMvc.perform(post("/exercises").content(asJsonString(postExercise2)).contentType(MediaType.APPLICATION_JSON)); + + } catch (Exception e) { + assert (false); + } } - private QuestionDto createQuestion(String id) throws Exception { + @BeforeEach + void init() { +// createExercise(); + + qston = new QuestionDto("\"what is the meaning of: costs an arm and leg\"",1,new ArrayList<>()); + long id = 2; + var question = new QuestionCreateDto("this statement is false", id, List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); + var question2 = new QuestionCreateDto("What month of the year has 28 days?", id, List.of(new AnswerInQuestionCreateDto("February", false), new AnswerInQuestionCreateDto("All of them", true))); + ResultActions posted = null; + } + @Test + void testCreateQuestion() throws Exception { + var exerciseId = 1L; + var question = new QuestionCreateDto("what is the meaning of: costs an arm and leg", exerciseId, List.of(new AnswerInQuestionCreateDto("dis very expencive", true), new AnswerInQuestionCreateDto("FMA refference", false))); + when(facade.create(ArgumentMatchers.isA(QuestionCreateDto.class))).thenReturn(qston); var posted = mockMvc.perform(post("/questions").content(asJsonString(question)).contentType(MediaType.APPLICATION_JSON)); + posted.andExpect(status().isCreated()) + .andExpect(jsonPath("$.text", is(qston.getText()))) + .andExpect(jsonPath("$.exerciseId", is((int)qston.getExerciseId()))); - var cont = posted.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(cont, QuestionDto.class); - return res; } - private String createExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, "0"); - - String id = ""; - - try { - var dis = mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); - var ok = dis.andReturn().getResponse().getContentAsString(); + @Test + void testCreateQuestionEmptyQuestions() throws Exception { + var prompt = """ + { + "text": "this is a question without exercise Id", + "exerciseId": 1, + "answers": [] + } + """; - var ll = objectMapper.readValue(ok, ExerciseDto.class); + var posted = mockMvc.perform(post("/questions").content(prompt).contentType(MediaType.APPLICATION_JSON)); - id = ll.getId(); - } catch (Exception e) { - assert (false); - } - return id; + posted.andExpect(status().isCreated()); } @Test - void getQuestion() throws Exception { - + void testCreateQuestionEmptyField() throws Exception { + var exerciseId = 1L; + var question = new QuestionCreateDto("", exerciseId, List.of(new AnswerInQuestionCreateDto("dis very expencive", true), new AnswerInQuestionCreateDto("FMA refference", false))); + var posted = mockMvc.perform(post("/questions").content(asJsonString(question)).contentType(MediaType.APPLICATION_JSON)); - String exerciseId = createExercise(); - var question = createQuestion(exerciseId); + posted.andExpect(status().is4xxClientError()); + } - var theId = String.format("/questions/%s", question.getId()); + @Test + void testCreateQuestionMissingField() throws Exception { + var prompt = """ + { + "text": "this is a question without exercise Id, + "answers" : [] + } + """; + var posted = mockMvc.perform(post("/questions").content(prompt).contentType(MediaType.APPLICATION_JSON)); - var gets = mockMvc.perform(get(theId)); + posted.andExpect(status().is4xxClientError()); + } - var content = gets.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(content, QuestionDto.class); + private QuestionDto createQuestion(long id) throws Exception { + var question = new QuestionCreateDto("this statement is false", id, List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); + var posted = mockMvc.perform(post("/questions").content(asJsonString(question)).contentType(MediaType.APPLICATION_JSON)); + var cont = posted.andReturn().getResponse().getContentAsString(); + var res = objectMapper.readValue(cont, QuestionDto.class); + return res; + } - assert res.equals(question); + @Test + void getQuestion() throws Exception { + var question = new QuestionDto("this statement is false",1L,new ArrayList<>()); + when(facade.find(1)).thenReturn(question); + var gets = mockMvc.perform(get("/questions/1")); + gets.andExpect(status().isOk()).andExpect(jsonPath("$.text", is("this statement is false"))); } @Test - void getByExercise() throws Exception { - - var exerciseId = createExercise(); - var question = createQuestion(exerciseId); + void getQuestionNotFound() throws Exception { + when(facade.find(9999)).thenThrow(new EntityNotFoundException()); + var gets = mockMvc.perform(get("/questions/9999")); + gets.andExpect(status().isNotFound()); + } - var theId = String.format("/questions/exercise/%s", exerciseId); - var smth = mockMvc.perform(get(theId).param("page", "0")); + @Test + void getAnswer() throws Exception { - var content = smth.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(content, new TypeReference<Result<QuestionDto>>() { - }); + var sss = List.of(new AnswerDto("February", false), new AnswerDto("All of them", true)); + when(facade.getQuestionAnswers(2)).thenReturn(sss); + var gets = mockMvc.perform(get("/questions/2/answers")); + gets.andExpect(status().isOk()) + .andExpect(jsonPath("$[0].text", is("February"))) + .andExpect(jsonPath("$[1].text", is("All of them"))); - Map<String, String> params; - assert (res.getItems().get(0).equals(question)); } @Test void TestUpdate() throws Exception { - String id = createExercise(); - var question = createQuestion(id); - var updated = """ { "text": "wat a paradox?", - "exerciseId": "%s" + "exerciseId": "1" } """; - updated = String.format(updated, id); - - var smth = mockMvc.perform(put(String.format("/questions/%s", question.getId())).content(updated).contentType(MediaType.APPLICATION_JSON)); - - var content = smth.andReturn().getResponse().getContentAsString(); - - var res = objectMapper.readValue(content, QuestionDto.class); + var question = new QuestionDto("wat a paradox?",1,new ArrayList<>()); - question.setText("wat a paradox?"); + when(facade.patchUpdate(ArgumentMatchers.eq(1), ArgumentMatchers.isA(QuestionUpdateDto.class))).thenReturn(question); + var gets = mockMvc.perform(put("/questions/1").content(updated).contentType(MediaType.APPLICATION_JSON)); - assert (question.equals(res)); + gets.andExpect(status().isOk()); + } +@Test + void deleteExisting() { + try { + mockMvc.perform(delete("/questions/1")).andExpect(status().isNoContent()); + } catch (Exception e) { + throw new RuntimeException(e); } } + +} diff --git a/application/module-language-school/Dockerfile b/application/module-language-school/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f2be765c3d8637d93aba9fefea412f0b0ae2edf1 --- /dev/null +++ b/application/module-language-school/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-language-school-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/module-language-school/pom.xml b/application/module-language-school/pom.xml index 42e3a5ded4aed76f99a3ae2be4c55d304e05b0d0..d54d174bf358a4ab6e62a74c5f219f2661ab89f4 100644 --- a/application/module-language-school/pom.xml +++ b/application/module-language-school/pom.xml @@ -59,8 +59,17 @@ <version>0.0.1-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-tx</artifactId> + </dependency> + <dependency> + <groupId>io.swagger</groupId> + <artifactId>swagger-annotations</artifactId> + <version>1.6.9</version> + </dependency> - </dependencies> + </dependencies> <build> <plugins> diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/DataInitializer.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/DataInitializer.java deleted file mode 100644 index 6355c4a2a37f09cc450e0e6e301a7c5a873277c6..0000000000000000000000000000000000000000 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/DataInitializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.fuseri.modulelanguageschool; - -import org.fuseri.modulelanguageschool.user.Address; -import org.fuseri.modulelanguageschool.user.User; -import org.fuseri.modulelanguageschool.user.UserService; -import org.fuseri.modulelanguageschool.user.UserType; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class DataInitializer implements ApplicationRunner { - - private final UserService userService; - - @Override - public void run(ApplicationArguments args) { - User user = User.builder() - .email("test@email.com") - .firstName("John") - .lastName("Doe") - .username("johnD") - .password("password") - .userType(UserType.ADMIN).address(Address.builder() - .city("Brno") - .country("CZ") - .street("HrnÄŤĂrska") - .houseNumber("99") - .build()) - .build(); - - userService.create(user); - } -} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainMapper.java index dfc4a7f1ec17e8beec0eb75ac1e4d9f8d305aea7..e80dbdb1a48c5b26ac2b9582ae4d426724e6f97f 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainMapper.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainMapper.java @@ -1,9 +1,7 @@ package org.fuseri.modulelanguageschool.common; import org.fuseri.model.dto.common.DomainObjectDto; -import org.fuseri.model.dto.common.Result; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import java.util.List; @@ -15,11 +13,7 @@ public interface DomainMapper<T extends DomainObject, S extends DomainObjectDto> List<S> toDtoList(List<T> entities); - @Mappings({ - @Mapping(target = "total", expression = "java(source.getTotalElements())"), - @Mapping(target = "page", expression = "java(source.getNumber())"), - @Mapping(target = "pageSize", expression = "java(source.getSize())"), - @Mapping(target = "items", expression = "java(toDtoList(source.getContent()))") - }) - Result<S> toResult(Page<T> source); + default Page<S> toDtoPage(Page<T> entities) { + return new PageImpl<>(toDtoList(entities.getContent()), entities.getPageable(), entities.getTotalPages()); + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java index 8520e8aa4ca12429b526281cce2f466ad735e299..a4ef86b4c2474a672041b2e05a4c875c9152fe29 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainObject.java @@ -1,18 +1,21 @@ package org.fuseri.modulelanguageschool.common; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import java.util.UUID; - @Getter @Setter +@EqualsAndHashCode(exclude = "id") @MappedSuperclass public abstract class DomainObject { @Id - private String id = UUID.randomUUID().toString(); + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainService.java index 706071fb66c60702f9d1495bbdf2f8e7db533f31..78dd3e88ff99481cb3bafda286a1727ab2394702 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainService.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/DomainService.java @@ -1,5 +1,6 @@ package org.fuseri.modulelanguageschool.common; +import jakarta.persistence.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -10,7 +11,7 @@ public abstract class DomainService<T extends DomainObject> { public static final int DEFAULT_PAGE_SIZE = 10; - public abstract JpaRepository<T, String> getRepository(); + public abstract JpaRepository<T, Long> getRepository(); @Transactional public T create(T entity) { @@ -26,4 +27,27 @@ public abstract class DomainService<T extends DomainObject> { public Page<T> findAll(int page) { return getRepository().findAll(PageRequest.of(page, DEFAULT_PAGE_SIZE)); } + + /** + * Delete an entity with specified id + * @param id id of the entity to delete + */ + public void delete(long id) { + getRepository().deleteById(id); + } + + /** + * Update an entity + * + * @param id the entity ID + * @param entity the entity to update + * @return the updated entity + */ + public T update(Long id, T entity) { + if (!getRepository().existsById(id)) { + throw new EntityNotFoundException("Entity with id " + entity.getId() + " not found."); + } + entity.setId(id); + return getRepository().save(entity); + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..2227c5640bba11c5405bc103d2cc4ddbd9f67c8e --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/common/ResourceNotFoundException.java @@ -0,0 +1,22 @@ +package org.fuseri.modulelanguageschool.common; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException() { + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java index 38f42dfa4b571e5e77cb362df20d89fc73d038ac..fd81844e735a953b1b07cf77678f3d2f48134da8 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/Course.java @@ -1,15 +1,20 @@ package org.fuseri.modulelanguageschool.course; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; +import jakarta.persistence.*; import lombok.*; import org.fuseri.modulelanguageschool.common.DomainObject; +import org.fuseri.modulelanguageschool.user.User; + +import java.util.List; +import java.util.Set; @Getter @Setter +@Entity +@Table(name = "course") @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) public class Course extends DomainObject { private String name; @@ -19,5 +24,28 @@ public class Course extends DomainObject { private Language language; @Enumerated(EnumType.STRING) - private ProficiencyLevel proficiencyLevel; + private ProficiencyLevel proficiency; + + @ManyToMany + private List<User> students; + + private boolean finished = false; + public void enrolStudent(User student) { + students.add(student); + } + + public void expelStudent(User student) { + students.remove(student); + } + + public Course(String name, Integer capacity, Language language, ProficiencyLevel proficiency) { + this.name = name; + this.capacity = capacity; + this.language = language; + this.proficiency = proficiency; + } + + public void setFinished(boolean finished) { + this.finished = finished; + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java index 9646fb8ff0c88dc974b065f545f17f365c7c0ded..2011884b168ead61234283c8fa115b16e059492c 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseController.java @@ -1,26 +1,40 @@ package org.fuseri.modulelanguageschool.course; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import jakarta.validation.Valid; -import jakarta.validation.constraints.PositiveOrZero; import org.fuseri.model.dto.course.CourseCreateDto; import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.course.LanguageTypeDto; import org.fuseri.model.dto.course.ProficiencyLevelDto; import org.fuseri.model.dto.user.UserDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.List; /** * This class represents a RESTful controller for Courses resources. * It handles incoming HTTP requests related to courses, and delegates them to the appropriate service method. */ +@Api(tags = "Course Controller") @RestController @RequestMapping("/courses") public class CourseController { - public static final String COURSE_NAME = "english b2 course"; + private final CourseFacade courseFacade; + + @Autowired + public CourseController(CourseFacade courseFacade) { + this.courseFacade = courseFacade; + } /** * Creates a new course. @@ -28,9 +42,15 @@ public class CourseController { * @param dto the CourseCreateDto containing the course data * @return the newly created CourseDto */ - @PostMapping("/create") - public CourseDto create(@Valid @RequestBody CourseCreateDto dto) { - return new CourseDto(dto.getName(), dto.getCapacity(), dto.getLanguageTypeDto(), dto.getProficiencyLevelDto()); + @ApiOperation(value = "Create a new course") + @PostMapping + @ApiResponses({ + @ApiResponse(code = 201, message = "Course created successfully"), + @ApiResponse(code = 400, message = "Invalid request body") + }) + public ResponseEntity<CourseDto> create(@Valid @RequestBody CourseCreateDto dto) { + CourseDto courseDto = courseFacade.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(courseDto); } /** @@ -39,9 +59,15 @@ public class CourseController { * @param id the ID of the course to retrieve * @return the CourseDto for the specified ID */ + @ApiOperation(value = "Retrieve a course by ID") @GetMapping("/find/{id}") - public CourseDto find(@PathVariable String id) { - return new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + @ApiResponses({ + @ApiResponse(code = 200, message = "Course found"), + @ApiResponse(code = 404, message = "Course not found") + }) + public ResponseEntity<CourseDto> find(@PathVariable Long id) { + CourseDto courseDto = courseFacade.findById(id); + return ResponseEntity.ok(courseDto); } /** @@ -50,36 +76,50 @@ public class CourseController { * @param page the page number to retrieve * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses") @GetMapping("/findAll") - public List<CourseDto> findAll(@RequestParam int page) { - return new ArrayList<>(); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Successfully retrieved courses"), + @ApiResponse(code = 404, message = "No courses found") + }) + public ResponseEntity<Page<CourseDto>> findAll(@RequestParam int page) { + Page<CourseDto> courseDtoPage = courseFacade.findAll(PageRequest.of(page, 10, Sort.by(Sort.Direction.ASC, "name"))); + return ResponseEntity.ok(courseDtoPage); } /** * Retrieves a paginated list of courses of a given language * - * @param page the page number to retrieve * @param lang the language to find courses of * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses of a given language") @GetMapping("/findAllByLang") - public List<CourseDto> findAll(@RequestParam int page, @RequestParam LanguageTypeDto lang) { - return new ArrayList<>(); + @ApiResponses({ + @ApiResponse(code = 200, message = "Courses found"), + @ApiResponse(code = 400, message = "Invalid request body") + }) + public ResponseEntity<List<CourseDto>> findAll(@RequestParam LanguageTypeDto lang) { + List<CourseDto> courseDtos = courseFacade.findAll(lang); + return ResponseEntity.ok(courseDtos); } - /** * Retrieves a paginated list of courses of a given language and proficiency * - * @param page the page number to retrieve * @param lang the language to find courses of * @param prof the proficiency of the language * @return the Result containing the requested page of CourseDtos */ + @ApiOperation(value = "Retrieve a paginated list of courses of a given language and proficiency") @GetMapping("/findAllByLangProf") - public List<CourseDto> findAll(@RequestParam int page, - @RequestParam LanguageTypeDto lang, + @ApiResponses({ + @ApiResponse(code = 200, message = "Courses found"), + @ApiResponse(code = 400, message = "Invalid request body") + }) + public ResponseEntity<List<CourseDto>> findAll(@RequestParam LanguageTypeDto lang, @RequestParam ProficiencyLevelDto prof) { - return new ArrayList<>(); + List<CourseDto> courses = courseFacade.findAll(lang, prof); + return ResponseEntity.ok(courses); } /** @@ -89,22 +129,34 @@ public class CourseController { * @param dto the CourseCreateDto containing the updated course data * @return the updated CourseDto */ + @ApiOperation(value = "Update an existing course") @PutMapping("/update/{id}") - public CourseDto update(@PathVariable String id, @Valid @RequestBody CourseCreateDto dto) { - return new CourseDto(dto.getName(), dto.getCapacity(), dto.getLanguageTypeDto(), dto.getProficiencyLevelDto()); + @ApiResponses({ + @ApiResponse(code = 200, message = "Course updated successfully"), + @ApiResponse(code = 400, message = "Invalid request body"), + @ApiResponse(code = 404, message = "Course not found") + }) + public ResponseEntity<CourseDto> update(@PathVariable Long id, @Valid @RequestBody CourseCreateDto dto) { + CourseDto updatedCourse = courseFacade.update(id, dto); + return ResponseEntity.ok(updatedCourse); } /** * Deletes a course by ID. * * @param id the ID of the course to delete - * @return true if the course was successfully deleted, false otherwise */ + @ApiOperation(value = "Delete a course by ID") @DeleteMapping("/delete/{id}") - public void delete(@PathVariable String id) { + @ApiResponses({ + @ApiResponse(code = 204, message = "Course deleted successfully"), + @ApiResponse(code = 404, message = "Course not found") + }) + public ResponseEntity<Void> delete(@PathVariable Long id) { + courseFacade.delete(id); + return ResponseEntity.noContent().build(); } - /** * Adds student to the existing course * @@ -112,11 +164,15 @@ public class CourseController { * @param student UserDto for the student * @return the CourseDto representing the updated course */ + @ApiOperation(value = "Add student to the existing course") @PatchMapping("/enrol/{id}") - public CourseDto enrol(@PathVariable String id, @RequestBody UserDto student) { - var course = new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); - course.setStudentIds(new ArrayList<>(List.of(student.getId()))); - return course; + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Successfully enrolled student in course"), + @ApiResponse(code = 404, message = "Course not found") + }) + public ResponseEntity<CourseDto> enrol(@PathVariable Long id, @RequestParam Long studentId) { + CourseDto updatedCourse = courseFacade.enrol(id, studentId); + return ResponseEntity.ok(updatedCourse); } /** @@ -126,9 +182,15 @@ public class CourseController { * @param student UserDto for the student * @return the CourseDto representing the updated course */ + @ApiOperation(value = "Remove student from the existing course") @PatchMapping("/expel/{id}") - public CourseDto expel(@PathVariable String id, @RequestBody UserDto student) { - return new CourseDto(COURSE_NAME, 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Successfully expelled student from course"), + @ApiResponse(code = 404, message = "Course not found") + }) + public ResponseEntity<CourseDto> expel(@PathVariable Long id, @RequestBody UserDto student) { + CourseDto updatedCourse = courseFacade.expel(id, student); + return ResponseEntity.ok(updatedCourse); } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..dba3056b203a76632fa11943d512f7c68d3e5901 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseFacade.java @@ -0,0 +1,77 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.fuseri.modulelanguageschool.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class CourseFacade { + private final CourseService courseService; + private final UserService userService; + private final CourseMapper courseMapper; + private final UserMapper userMapper; + + @Autowired + public CourseFacade(CourseService courseService, UserService userService, CourseMapper courseMapper, UserMapper userMapper) { + this.courseService = courseService; + this.userService = userService; + this.courseMapper = courseMapper; + this.userMapper = userMapper; + } + + @Transactional + public CourseDto create(CourseCreateDto dto) { + return courseMapper.mapToDto(courseService.save(courseMapper.mapToCourse(dto))); + } + + @Cacheable(cacheNames = "courses", key = "#id") + @Transactional(readOnly = true) + public CourseDto findById(Long id) { + return courseMapper.mapToDto(courseService.findById(id)); + } + + @Transactional(readOnly = true) + public Page<CourseDto> findAll(Pageable pageable) { + return courseMapper.mapToPageDto(courseService.findAll(pageable)); + } + + @Transactional + public CourseDto update(Long id, CourseCreateDto dto) { + return courseMapper.mapToDto(courseService.update(id, courseMapper.mapToCourse(dto))); + } + + @Transactional + public void delete(Long id) { + courseService.delete(id); + } + + public List<CourseDto> findAll(LanguageTypeDto lang) { + return courseMapper.mapToList(courseService.findAll(Language.valueOf(lang.name()))); + } + + public List<CourseDto> findAll(LanguageTypeDto lang, ProficiencyLevelDto prof) { + return courseMapper.mapToList(courseService.findAll(Language.valueOf(lang.name()), ProficiencyLevel.valueOf(prof.name()))); + } + + public CourseDto enrol(Long id, Long studentId) { + var student = userService.find(studentId); + return courseMapper.mapToDto(courseService.enrol(id, student)); + } + + public CourseDto expel(Long id, UserDto student) { + return courseMapper.mapToDto(courseService.expel(id, userMapper.fromDto(student))); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..9a5bf7c032fb11df53ef65ddad42b0e82230a7bf --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseMapper.java @@ -0,0 +1,27 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CourseMapper { + + CourseDto mapToDto(Course course); + + Course mapToCourse(CourseDto courseDto); + + List<CourseDto> mapToList(List<Course> persons); + + + default Page<CourseDto> mapToPageDto(Page<Course> courses) { + return new PageImpl<>(mapToList(courses.getContent()), courses.getPageable(), courses.getTotalPages()); + } + + Course mapToCourse(CourseCreateDto dto); +} + diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..647d778b5dcb87c271bddaa2ba845e76668c5b3f --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseRepository.java @@ -0,0 +1,21 @@ +package org.fuseri.modulelanguageschool.course; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CourseRepository extends JpaRepository<Course, Long> { + + + @Query("SELECT c FROM Course c left join fetch User u WHERE c.language = ?1 AND u.userType!=\"ADMIN\"") + Course getById(Long id); + + @Query("SELECT c FROM Course c WHERE c.language = ?1") + List<Course> findAllByLang(Language language); + + @Query("SELECT c FROM Course c WHERE c.language = ?1 AND c.proficiency = ?2") + List<Course> findAllByLangProf(Language language, ProficiencyLevel proficiencyLevel); +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java new file mode 100644 index 0000000000000000000000000000000000000000..4e54b9f91bba5849884d37e2ac291c8dd9ee000b --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/course/CourseService.java @@ -0,0 +1,94 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.modulelanguageschool.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +@Service +public class CourseService { + + private final CourseRepository courseRepository; + + @Autowired + public CourseService(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + } + + @Transactional + public Course save(Course course) { + return courseRepository.save(course); + } + + @Transactional(readOnly = true) + public Course findById(Long id) { + return courseRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Course with id: " + id + " was not found.")); + } + + @Transactional(readOnly = true) + public Page<Course> findAll(Pageable pageable) { + return courseRepository.findAll(pageable); + } + + @Transactional + public Course update(Long id, Course newCourse) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.setName(newCourse.getName()); + course.setCapacity(newCourse.getCapacity()); + course.setLanguage(newCourse.getLanguage()); + course.setProficiency(newCourse.getProficiency()); + return courseRepository.save(course); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Course with id: " + id + " was not found."); + } + } + + @Transactional + public void delete(Long id) { + courseRepository.deleteById(id); + } + + public List<Course> findAll(Language language) { + return courseRepository.findAllByLang(language); + } + + public List<Course> findAll(Language language, ProficiencyLevel proficiencyLevel) { + return courseRepository.findAllByLangProf(language, proficiencyLevel); + } + + public Course enrol(Long id, User student) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.enrolStudent(student); + return courseRepository.save(course); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Course with id: " + id + " was not found."); + } + } + + public Course expel(Long id, User student) { + Optional<Course> optionalCourse = courseRepository.findById(id); + if (optionalCourse.isPresent()) { + Course course = optionalCourse.get(); + course.expelStudent(student); + return courseRepository.save(course); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Course with id: " + id + " was not found."); + } + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java index 86b9670a578674a85557aad6768c022561a1b53a..14afc0a256929f0c98f8ad3aa461896488d95189 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/Lecture.java @@ -3,10 +3,8 @@ package org.fuseri.modulelanguageschool.lecture; import jakarta.persistence.Entity; import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import jakarta.persistence.Table; +import lombok.*; import org.fuseri.modulelanguageschool.common.DomainObject; import org.fuseri.modulelanguageschool.course.Course; import org.fuseri.modulelanguageschool.user.User; @@ -19,17 +17,32 @@ import java.util.List; */ @Getter @Setter +@Entity +@Table(name = "lecture") @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) public class Lecture extends DomainObject { - private LocalDateTime from; - private LocalDateTime to; + private LocalDateTime lectureFrom; + private LocalDateTime lectureTo; private String topic; + private Integer capacity; + @ManyToOne private Course course; + @ManyToOne private User lecturer; - private List<User> user; + @ManyToMany + private List<User> students; + + public void enrolStudent(User student) { + students.add(student); + } + + public void expelStudent(User student) { + students.remove(student); + } } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java index 34d91578d4a8f53d4dbc75972d3216cbb199adf6..80b4f8e154f831ff52fc87745e0596c9f57b8c66 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureController.java @@ -1,28 +1,36 @@ package org.fuseri.modulelanguageschool.lecture; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import jakarta.validation.Valid; -import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.lecture.LectureCreateDto; import org.fuseri.model.dto.lecture.LectureDto; import org.fuseri.model.dto.user.UserDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.time.Month; -import java.util.ArrayList; import java.util.List; /** * This class represents a RESTful controller for Lecture resources. * It handles incoming HTTP requests related to lectures, and delegates them to the appropriate service method. */ +@Api(tags = "Lecture Controller") @RestController @RequestMapping("/lectures") public class LectureController { - private static final LocalDateTime START_DATETIME = LocalDateTime.of(2045, Month.JUNE, 30, 12, 0, 0); - private static final LocalDateTime END_DATETIME = LocalDateTime.of(2045, Month.JUNE, 30, 14, 0, 0); - public static final String TOPIC = "Learning how to spell deprecated"; + private final LectureFacade lectureFacade; + + @Autowired + public LectureController(LectureFacade lectureFacade) { + this.lectureFacade = lectureFacade; + } + /** * Creates a new lecture resource by delegating to the LectureService's create method. @@ -30,20 +38,32 @@ public class LectureController { * @param lecture the LectureDto representing the lecture to be created * @return the LectureDto representing the newly created lecture */ - @PostMapping("/create") - public LectureDto create(@Valid @RequestBody LectureCreateDto lecture) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiOperation(value = "Create a new lecture") + @PostMapping + @ApiResponses(value = { + @ApiResponse(code = 201, message = "The lecture has been successfully created"), + @ApiResponse(code = 400, message = "The request body is invalid") + }) + public ResponseEntity<LectureDto> create(@Valid @RequestBody LectureCreateDto lecture) { + LectureDto lectureDto = lectureFacade.create(lecture); + return ResponseEntity.status(HttpStatus.CREATED).body(lectureDto); } /** * Retrieves a lecture resource by its ID. * - * @param id the ID of the lecture to find + * @param courseId the ID of the lecture to find * @return the LectureDto representing the found lecture */ - @GetMapping("find/{id}") - public LectureDto find(@PathVariable String id) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiOperation(value = "Retrieve a lecture by its ID") + @GetMapping("find/{courseId}") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The lecture has been found"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<LectureDto> find(@PathVariable Long courseId) { + LectureDto lectureDto = lectureFacade.findById(courseId); + return ResponseEntity.ok(lectureDto); } /** @@ -52,9 +72,14 @@ public class LectureController { * @param courseId the course to retrieve lectures from * @return the list of LectureDtos */ + @ApiOperation(value = "Retrieve a list of lectures for the corresponding course") @GetMapping("/findByCourse") - public List<LectureDto> findByCourse(@Valid @RequestParam String courseId) { - return new ArrayList<>(); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The list of lectures has been found"), + @ApiResponse(code = 404, message = "The course with the specified ID does not exist") + }) + public ResponseEntity<List<LectureDto>> findByCourse(@Valid @RequestParam Long courseId) { + return ResponseEntity.ok(lectureFacade.findAll(courseId)); } /** @@ -63,9 +88,15 @@ public class LectureController { * @param lecture the CourseCreateDto representing the updated lecture * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Update an existing lecture") @PutMapping("/update/{id}") - public LectureDto update(@PathVariable String id, @Valid @RequestBody LectureCreateDto lecture) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The lecture has been successfully updated"), + @ApiResponse(code = 400, message = "The request body is invalid"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<LectureDto> update(@PathVariable Long id, @Valid @RequestBody LectureCreateDto lecture) { + return ResponseEntity.ok(lectureFacade.update(id, lecture)); } /** @@ -73,21 +104,34 @@ public class LectureController { * * @param id the ID of the lecture to delete */ + @ApiOperation(value = "Delete a lecture by its ID") @DeleteMapping("/delete/{id}") - public void delete(@PathVariable String id) { + @ApiResponses(value = { + @ApiResponse(code = 204, message = "The lecture has been successfully deleted"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<Void> delete(@PathVariable Long id) { + lectureFacade.delete(id); + return ResponseEntity.noContent().build(); } /** * Adds lecturer to the existing lecture resource * - * @param id id of lecture to update - * @param lecturer UserDto for the course lecturer + * @param id id of lecture to update + * @param lecturerDto UserDto for the course lecturer * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Add lecturer to the existing lecture") @PatchMapping("/setLecturer/{id}") - public LectureDto setLecturer(@PathVariable String id, @RequestBody UserDto lecturer) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The lecture has been successfully updated"), + @ApiResponse(code = 400, message = "The request body is invalid"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<LectureDto> setLecturer(@PathVariable Long id, @RequestBody UserDto lecturerDto) { + return ResponseEntity.ok(lectureFacade.setLecturer(id, lecturerDto)); } /** @@ -97,11 +141,15 @@ public class LectureController { * @param student UserDto for the course student * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Add student to the existing lecture") @PatchMapping("/enrol/{id}") - public LectureDto enrol(@PathVariable String id, @RequestBody UserDto student) { - var lecture = new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); - lecture.setStudentIds(new ArrayList<>(List.of(student.getId()))); - return lecture; + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The lecture has been successfully updated"), + @ApiResponse(code = 400, message = "The request body is invalid"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<LectureDto> enrol(@PathVariable Long id, @RequestBody UserDto student) { + return ResponseEntity.ok(lectureFacade.enrol(id, student)); } /** @@ -111,8 +159,14 @@ public class LectureController { * @param student UserDto for the course student * @return the LectureDto representing the updated lecture */ + @ApiOperation(value = "Remove student from the existing lecture") @PatchMapping("/expel/{id}") - public LectureDto expel(@PathVariable String id, @RequestBody UserDto student) { - return new LectureDto(START_DATETIME, END_DATETIME, TOPIC, 10, "0", "0"); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "The lecture has been successfully updated"), + @ApiResponse(code = 400, message = "The request body is invalid"), + @ApiResponse(code = 404, message = "The lecture with the specified ID does not exist") + }) + public ResponseEntity<LectureDto> expel(@PathVariable Long id, @RequestBody UserDto student) { + return ResponseEntity.ok(lectureFacade.expel(id, student)); } } \ No newline at end of file diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..ce9214dd71a6f10b6a9aa47187692e3ff6470fd7 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureFacade.java @@ -0,0 +1,81 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.course.CourseService; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class LectureFacade { + + private final LectureService lectureService; + private final LectureMapper lectureMapper; + private final UserMapper userMapper; + private final CourseService courseService; + + @Autowired + public LectureFacade(LectureService lectureService, LectureMapper lectureMapper, UserMapper userMapper, CourseService courseService) { + this.lectureService = lectureService; + this.lectureMapper = lectureMapper; + this.userMapper = userMapper; + this.courseService = courseService; + } + + @Transactional + public LectureDto create(LectureCreateDto dto) { + return lectureMapper.mapToDto(lectureService.save(lectureMapper.mapToLecture(dto, courseService))); + } + + @Cacheable(cacheNames = "courses", key = "#id") + @Transactional(readOnly = true) + public LectureDto findById(Long id) { + return lectureMapper.mapToDto(lectureService.findById(id)); + } + + @Transactional(readOnly = true) + public List<LectureDto> findAll(Long id) { + return lectureMapper.mapToList(lectureService.findAllByCourse(id)); + } + + @Transactional + public LectureDto update(Long id, LectureCreateDto dto) { + return lectureMapper.mapToDto(lectureService.update(id, lectureMapper.mapToLecture(dto, courseService))); + } + + @Transactional + public void delete(Long id) { + lectureService.delete(id); + } + + public List<LectureDto> findAll(LanguageTypeDto lang) { + return lectureMapper.mapToList(lectureService.findAll(Language.valueOf(lang.name()))); + } + + public List<LectureDto> findAll(LanguageTypeDto lang, ProficiencyLevelDto prof) { + return lectureMapper.mapToList(lectureService.findAll(Language.valueOf(lang.name()), ProficiencyLevel.valueOf(prof.name()))); + } + + public LectureDto enrol(Long id, UserDto student) { + return lectureMapper.mapToDto(lectureService.enrol(id, userMapper.fromDto(student))); + } + + public LectureDto expel(Long id, UserDto student) { + return lectureMapper.mapToDto(lectureService.expel(id, userMapper.fromDto(student))); + } + + public LectureDto setLecturer(Long id, UserDto lecturerDto) { + return lectureMapper.mapToDto(lectureService.setLecturer(id, userMapper.fromDto(lecturerDto))); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..1eba68d367b58e75f46bf841871aae883cc25a0d --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureMapper.java @@ -0,0 +1,71 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.fuseri.modulelanguageschool.course.CourseService; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserService; +import org.mapstruct.Mapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring", uses = {UserService.class}) +public interface LectureMapper { + + default LectureDto mapToDto(Lecture lecture) { + if (lecture == null) { + return null; + } + + var dto = new LectureDto(lecture.getLectureFrom(), + lecture.getLectureTo(), + lecture.getTopic(), + lecture.getCapacity(), + lecture.getLecturer() == null ? null : lecture.getLecturer().getId(), + lecture.getCourse().getId(), + new ArrayList<>(lecture.getStudents().stream().map(User::getId).collect(Collectors.toList()))); + dto.setId(lecture.getId()); + return dto; + } + + default Lecture mapToLecture(LectureDto lectureDto, CourseService courseService, UserService userService) { + if (lectureDto == null) { + return null; + } + var lecture = new Lecture(lectureDto.getLectureFrom(), + lectureDto.getLectureTo(), + lectureDto.getTopic(), + lectureDto.getCapacity(), + courseService.findById(lectureDto.getCourseId()), + userService.find(lectureDto.getLecturerId()), + new ArrayList<>()); + lecture.setId(lectureDto.getId()); + for (Long userId : lectureDto.getStudents()) { + lecture.enrolStudent(userService.find(userId)); + } + return lecture; + } + + default List<LectureDto> mapToList(List<Lecture> lectures) { + if (lectures == null) { + return null; + } + return new ArrayList<>(lectures.stream().map(this::mapToDto).toList()); + } + + + default Lecture mapToLecture(LectureCreateDto lectureDto, CourseService courseService) { + if (lectureDto == null) { + return null; + } + return new Lecture(lectureDto.getLectureFrom(), + lectureDto.getLectureTo(), + lectureDto.getTopic(), + lectureDto.getCapacity(), + courseService.findById(lectureDto.getCourseId()), + null, + new ArrayList<>()); + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..1fd35bce8e435fd2bb321504daabf03772997fc3 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureRepository.java @@ -0,0 +1,21 @@ +package org.fuseri.modulelanguageschool.lecture; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LectureRepository extends JpaRepository<Lecture, Long> { + + @Query("SELECT l FROM Lecture l WHERE l.course.id = ?1") + List<Lecture> findAllByCourse(Long id); + + @Query("SELECT l FROM Lecture l WHERE l.course.language = ?1") + List<Lecture> findAllByLang(Language language); + + @Query("SELECT l FROM Lecture l WHERE l.course.language = ?1 AND l.course.proficiency = ?2") + List<Lecture> findAllByLangProf(Language language, ProficiencyLevel proficiencyLevel); +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java new file mode 100644 index 0000000000000000000000000000000000000000..a3d1ea7f96aaa79883689189331e88977aede3eb --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/lecture/LectureService.java @@ -0,0 +1,113 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +@Service +public class LectureService { + + private final LectureRepository lectureRepository; + + @Autowired + public LectureService(LectureRepository lectureRepository) { + this.lectureRepository = lectureRepository; + } + + @Transactional + public Lecture save(Lecture lecture) { + return lectureRepository.save(lecture); + } + + @Transactional(readOnly = true) + public Lecture findById(Long id) { + return lectureRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Lecture with id: " + id + " was not found.")); + } + + @Transactional(readOnly = true) + public List<Lecture> findAllByCourse(Long id) { + return lectureRepository.findAllByCourse(id); + } + + @Transactional + public Lecture update(Long id, Lecture newLecture) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.setLectureFrom(newLecture.getLectureFrom()); + lecture.setLectureTo(newLecture.getLectureTo()); + lecture.setTopic(newLecture.getTopic()); + lecture.setCourse(newLecture.getCourse()); + lecture.setLecturer(newLecture.getLecturer()); + lecture.setStudents(newLecture.getStudents()); + return lectureRepository.save(lecture); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Lecture with id: " + id + " was not found."); + } + } + + @Transactional + public void delete(Long id) { + lectureRepository.deleteById(id); + } + + @Transactional + public List<Lecture> findAll(Language language) { + return lectureRepository.findAllByLang(language); + } + + @Transactional + public List<Lecture> findAll(Language language, ProficiencyLevel proficiencyLevel) { + return lectureRepository.findAllByLangProf(language, proficiencyLevel); + } + + @Transactional + public Lecture enrol(Long id, User student) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.enrolStudent(student); + return lectureRepository.save(lecture); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Lecture with id: " + id + " was not found."); + } + } + + @Transactional + public Lecture expel(Long id, User student) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.expelStudent(student); + return lectureRepository.save(lecture); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Lecture with id: " + id + " was not found."); + } + } + + @Transactional + public Lecture setLecturer(Long id, User lecturer) { + Optional<Lecture> optionalLecture = lectureRepository.findById(id); + if (optionalLecture.isPresent()) { + Lecture lecture = optionalLecture.get(); + lecture.setLecturer(lecturer); + return lectureRepository.save(lecture); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Lecture with id: " + id + " was not found."); + } + } +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/Address.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/Address.java index 5ab818f731825df1fe4980d6d362acc0908609e7..f9149c09277e3eae763434b6beb8ae203e346540 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/Address.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/Address.java @@ -8,6 +8,7 @@ import lombok.*; @Builder @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode @Embeddable public class Address { diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/User.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/User.java index 7b0439da0ed0c77d78ec4f2dcaee7a7511e998e9..d1fb12b86bd192bb071466e304f91de027330037 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/User.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/User.java @@ -1,13 +1,14 @@ package org.fuseri.modulelanguageschool.user; -import org.fuseri.modulelanguageschool.common.DomainObject; import jakarta.persistence.*; import lombok.*; +import org.fuseri.modulelanguageschool.common.DomainObject; import org.fuseri.modulelanguageschool.course.Course; import org.fuseri.modulelanguageschool.course.Language; import org.fuseri.modulelanguageschool.course.ProficiencyLevel; -import java.util.List; +import java.util.Map; +import java.util.Set; @Getter @Setter @@ -15,6 +16,7 @@ import java.util.List; @Builder @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) @Table(name = "domain_user") public class User extends DomainObject { @@ -33,6 +35,18 @@ public class User extends DomainObject { @Embedded private Address address; - private List<Language> languages; - private List<ProficiencyLevel> proficiencyLevels; -} + + @ManyToMany + @JoinTable(name = "user_course", + joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id") + ) + private Set<Course> courses; + + @ElementCollection + private Map<Language, ProficiencyLevel> languageProficiency; + + public void addLanguageProficiency(Language language, ProficiencyLevel proficiencyLevel) { + languageProficiency.put(language, proficiencyLevel); + } +} \ No newline at end of file diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java index d0c6429b245ad650d6e5fe747d9db2fb48e88681..b46f7761502683ef3112fd12b61237f11073d6f8 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java @@ -1,90 +1,145 @@ package org.fuseri.modulelanguageschool.user; -import org.fuseri.model.dto.common.Result; -import org.fuseri.model.dto.user.AddressDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.user.UserAddLanguageDto; import org.fuseri.model.dto.user.UserCreateDto; import org.fuseri.model.dto.user.UserDto; import org.fuseri.model.dto.user.UserLoginDto; -import org.fuseri.modulelanguageschool.course.Course; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import jakarta.validation.Valid; -import jakarta.validation.constraints.PositiveOrZero; - -import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/users") public class UserController { - private final UserService service; - - private final UserMapper mapper; + private final UserFacade facade; @Autowired - public UserController(UserService service/*, UserMapper mapper*/) { - this.service = service; - this.mapper = null; + public UserController(UserFacade facade) { + this.facade = facade; } + @Operation(summary = "Get a user by Id", description = "Returns a user with specified Id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User with the specified Id is retrieved Successfuly", + content = @Content(schema = @Schema(implementation = UserDto.class) + )), + @ApiResponse(responseCode = "404", description = "User with the specified ID was not found.") + }) @GetMapping("/{id}") - public UserDto find(@PathVariable String id) { - return new UserDto("spracher","spracher@gmail.com","Sprach","MeNot",new AddressDto()); + public ResponseEntity<UserDto> find(@PathVariable @NotNull Long id) { + try { + return ResponseEntity.ok(facade.find(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } + @Operation(summary = "Create a User", description = "Creates a new User.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User created successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input.") + }) @PostMapping - public UserDto create(@Valid @RequestBody UserCreateDto dto) { - return new UserDto(dto.getUsername(),dto.getEmail(),dto.getFirstName(),dto.getLastName(),dto.getAddress()); + public ResponseEntity<UserDto> create(@Valid @RequestBody UserCreateDto dto) { + UserDto user = facade.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(user); } + @Operation(summary = "Delete a User with specified ID", description = "Deletes a User with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "User with the specified ID deleted successfully."), + }) @DeleteMapping("/{id}") - public UserDto deleteUser(@PathVariable String id) { - return new UserDto("spracher","spracher@gmail.com","Sprach","MeNot",new AddressDto()); + public ResponseEntity<Void> deleteUser(@PathVariable @NotNull Long id) { + facade.delete(id); + return ResponseEntity.noContent().build(); } - @PutMapping("/update/{id}") - public UserDto update(@PositiveOrZero @PathVariable String id,@Valid @RequestBody UserCreateDto user) { - - return new UserDto(user.getUsername(),user.getEmail(),user.getFirstName(),user.getLastName(),user.getAddress()); + @Operation(summary = "Update a User", description = "Updates a User with the specified ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User with the specified ID updated successfully."), + @ApiResponse(responseCode = "400", description = "Invalid input."), + @ApiResponse(responseCode = "404", description = "User with the specified ID was not found.") + }) + @PutMapping("/{id}") + public ResponseEntity<UserDto> update(@NotNull @PathVariable Long id, @Valid @RequestBody UserCreateDto dto) { + try { + return ResponseEntity.ok(facade.update(id, dto)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } - @GetMapping("/all") - public Result<UserDto> findAll(@PositiveOrZero @RequestParam int page) { - var res = new Result<UserDto>(); - res.setItems(List.of(new UserDto("spracher","spracher@gmail.com","Sprach","MeNot",new AddressDto()) -)); - - return res; - + @Operation(summary = "Get Users in paginated format", description = "Returns Users in paginated format.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved paginated Users"), + @ApiResponse(responseCode = "400", description = "Invalid page number supplied"), + }) + @GetMapping + public ResponseEntity<Page<UserDto>> findAll(@PositiveOrZero @NotNull @RequestParam int page) { + var a = facade.findAll(page); + return ResponseEntity.ok(a); } + //TODO: add authentication M3? @PostMapping("/login") - public String login(@Valid @RequestBody UserLoginDto dto) { - return String.format("User %s has spawned", dto.getUsername()); + public ResponseEntity<String> login(@RequestBody @Valid UserLoginDto dto) { + return ResponseEntity.ok(String.format("User %s has spawned", dto.getUsername())); } - - @PostMapping("/logout/{id}") - public String logout(@PathVariable String id) { - return "user has logged out"; + //TODO: add authentication M3? + @PostMapping("/{id}/logout") + public ResponseEntity<String> logout(@PathVariable @NotNull Long id) { + return ResponseEntity.ok("user has logged out"); } - @GetMapping("/finished/{id}") - public List<Course> getFinished(@PathVariable String id) { - return new ArrayList<>(); + @Operation(summary = "get finished courses", description = "retrieves finished courses of user with given Id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved finished courses"), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @GetMapping("/{id}/finished-courses") + public ResponseEntity<List<CourseDto>> getFinished(@PathVariable @NotNull Long id) { + return ResponseEntity.ok(facade.getFinished(id)); } - @GetMapping("/enrolled/{id}") - public List<Course> getEnrolled(@PathVariable String id) { - return new ArrayList<>(); + @Operation(summary = "get enrolled courses", description = "retrieves currently enrolled courses of user with given Id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved enrolled courses"), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @GetMapping("/{id}/courses") + public ResponseEntity<List<CourseDto>> getEnrolled(@PathVariable @NotNull Long id) { + return ResponseEntity.ok(facade.getEnrolled(id)); } - @PutMapping("/addLanguage/{id}") - public String addLanguage(@PathVariable String id,@Valid @RequestBody UserAddLanguageDto body) { - return "added Absolutely Nothing successfully!"; + @Operation(summary = "adds a language", description = "adds a new language and proficiency to user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully added a language"), + @ApiResponse(responseCode = "404", description = "User with given Id does not exist"), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PutMapping("/{id}/languages") + public ResponseEntity<UserDto> addLanguage(@PathVariable @NotNull Long id, @Valid @RequestBody UserAddLanguageDto body) { + try { + return ResponseEntity.ok(facade.addLanguageProficiency(id, body)); + } catch (EntityNotFoundException e) { + return ResponseEntity.notFound().build(); + } } - } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserFacade.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..9d822ff863d163ffb110a88fe96d67297c2ca8a3 --- /dev/null +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserFacade.java @@ -0,0 +1,74 @@ +package org.fuseri.modulelanguageschool.user; + +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.user.UserAddLanguageDto; +import org.fuseri.model.dto.user.UserCreateDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.common.DomainService; +import org.fuseri.modulelanguageschool.course.CourseMapper; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class UserFacade { + + private final UserMapper mapper; + private final CourseMapper courseMapper; + private final UserService service; + + @Autowired + public UserFacade(UserMapper mapper, CourseMapper courseMapper, UserService service) { + this.mapper = mapper; + this.courseMapper = courseMapper; + this.service = service; + } + + public UserDto find(Long id) { + return mapper.toDto(service.find(id)); + } + + public UserDto create(UserCreateDto dto) { + var user = mapper.fromCreateDto(dto); + return mapper.toDto(service.create(user)); + } + + public void delete(Long id) { + service.delete(id); + } + + public UserDto update(Long id, UserCreateDto dto) { + var user = mapper.fromCreateDto(dto); + var result = service.update(id, user); + return mapper.toDto(result); + + } + + public Page<UserDto> findAll(int page) { + PageRequest pageRequest = PageRequest.of(page, DomainService.DEFAULT_PAGE_SIZE); + return mapper.toDtoPage(service.findAll(pageRequest)); + } + + public UserDto addLanguageProficiency(Long id, UserAddLanguageDto body) { + var language = Language.valueOf(body.getLanguage().name()); + var proficiency = ProficiencyLevel.valueOf(body.getProficiency().name()); + User user = service.addLanguageProficiency(id, language, proficiency); + return mapper.toDto(user); + } + + public List<CourseDto> getEnrolled(Long id) { + return courseMapper.mapToList(service.getEnrolled(id)); + } + + public List<CourseDto> getFinished(Long id) { + return courseMapper.mapToList(service.getFinished(id)); + } + +} diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserMapper.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserMapper.java index 5fdcdcf85c04fd5f8af272c3bd75c158fecbe878..53d8c2458a40260b3cf67ef3e8f1281a8ca36ef7 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserMapper.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserMapper.java @@ -1,8 +1,8 @@ package org.fuseri.modulelanguageschool.user; -import org.fuseri.modulelanguageschool.common.DomainMapper; import org.fuseri.model.dto.user.UserCreateDto; import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.common.DomainMapper; import org.mapstruct.Mapper; @Mapper diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserRepository.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserRepository.java index 32d06b1313423619b849410606b36283efa90802..7651bda1996f2475cf8653edc7b24c21573354d1 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserRepository.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserRepository.java @@ -1,8 +1,19 @@ package org.fuseri.modulelanguageschool.user; +import org.fuseri.modulelanguageschool.course.Course; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository -public interface UserRepository extends JpaRepository<User, String> { +public interface UserRepository extends JpaRepository<User, Long> { + + @Query("SELECT c FROM Course c Left Join FETCH User d WHERE d.id = :id AND c.finished=FALSE") + List<Course> getEnrolled(Long id); + + @Query("SELECT c FROM Course c Left Join FETCH User d WHERE d.id = :id AND c.finished=TRUE") + List<Course> getFinished(Long id); + } diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserService.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserService.java index 1fc03b8ca97b71263d0b3c2f962cb275890af690..72b25dbe4b99b6890114df9ff77ac7de8ca49985 100644 --- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserService.java +++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserService.java @@ -1,12 +1,18 @@ package org.fuseri.modulelanguageschool.user; -import org.fuseri.modulelanguageschool.common.DomainService; import jakarta.persistence.EntityNotFoundException; import lombok.Getter; +import org.fuseri.modulelanguageschool.common.DomainService; +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + @Service public class UserService extends DomainService<User> { @@ -19,9 +25,27 @@ public class UserService extends DomainService<User> { } @Transactional(readOnly = true) - public User find(String id) { + public User find(Long id) { return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException("User '" + id + "' not found.")); } + public List<Course> getEnrolled(Long id) { + return repository.getEnrolled(id); + } + + public List<Course> getFinished(Long id) { + return repository.getFinished(id); + } + + public User addLanguageProficiency(Long id, Language language, ProficiencyLevel proficiency) { + Optional<User> optionalUser = repository.findById(id); + if (optionalUser.isPresent()) { + User user = optionalUser.get(); + user.addLanguageProficiency(language, proficiency); + return repository.save(user); + } else { + throw new EntityNotFoundException("User '" + id + "' not found."); + } + } } diff --git a/application/module-language-school/src/main/resources/application.properties b/application/module-language-school/src/main/resources/application.properties index 888dcec44dfa5c7cece2f03962df19175a89349e..08d8fcc59c97de1773876715a96a126e0831521e 100644 --- a/application/module-language-school/src/main/resources/application.properties +++ b/application/module-language-school/src/main/resources/application.properties @@ -1 +1,13 @@ -server.port=5000 \ No newline at end of file +server.port=5000 + +spring.jpa.open-in-view=false +spring.datasource.url=jdbc:h2:mem:social-network;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=SedaQ-app +spring.datasource.password=$argon2id$v=19$m=16,t=2,p=1$YmF0bWFuYmF0bWFu$MdHYB359HdivAb9J6CaILw +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +# showing SQL is generally good practice for running project locally to check whether there is not an issue with implementation of JPA methods. +spring.jpa.show-sql=true +spring.jackson.property-naming-strategy=LOWER_CAMEL_CASE +spring.cache.type=NONE +appconfig.enablecache=false \ No newline at end of file diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseControllerTest.java similarity index 51% rename from application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java rename to application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseControllerTest.java index e4fd8f0e5c56d80e54d57c39780620ed4aa07407..9aeca9f70e9c027d6ed34b4eaa61f7f2d90e0e8e 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseControllerTest.java @@ -7,6 +7,7 @@ import org.fuseri.model.dto.course.LanguageTypeDto; import org.fuseri.model.dto.course.ProficiencyLevelDto; import org.fuseri.model.dto.user.AddressDto; import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -14,52 +15,64 @@ 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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc -public class CourseTest { +public class CourseControllerTest { + + private final CourseCreateDto courseCreateDto = new CourseCreateDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + private final CourseDto courseDto = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + @Autowired ObjectMapper objectMapper; + @Autowired private MockMvc mockMvc; + @MockBean - private CourseController courseController; + private CourseFacade courseFacade; - private final CourseCreateDto courseCreateDto = new CourseCreateDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2);; - private final CourseDto courseDto = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } @Test void createCourse() throws Exception { - Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create") + Mockito.when(courseFacade.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); + mockMvc.perform(post("/courses") .content(asJsonString(courseCreateDto)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) + .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.id").exists()) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")); } @Test void createInvalidCourse() throws Exception { CourseCreateDto invalidCourseCreateDto = new CourseCreateDto(null, null, null, null); - Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create") + mockMvc.perform(post("/courses") .content(asJsonString(invalidCourseCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); @@ -67,115 +80,85 @@ public class CourseTest { @Test void createCourseWithoutParameter() throws Exception { - Mockito.when(courseController.create(ArgumentMatchers.isA(CourseCreateDto.class))).thenReturn(courseDto); - mockMvc.perform(post("/courses/create")) + mockMvc.perform(post("/courses")) .andExpect(status().is4xxClientError()); } + @Test void findCourse() throws Exception { - String id = "0"; - Mockito.when(courseController.find(id)).thenReturn(courseDto); + Long id = 0L; + Mockito.when(courseFacade.findById(id)).thenReturn(courseDto); mockMvc.perform(get("/courses/find/" + id)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.id").value(id)) + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) .andReturn().getResponse().getContentAsString(); } @Test void findCourseWithoutId() throws Exception { - Mockito.when(courseController.find(ArgumentMatchers.anyString())).thenReturn(courseDto); mockMvc.perform(get("/courses/find/")) .andExpect(status().is4xxClientError()); } @Test void findAll() throws Exception { - int page = 0; - Mockito.when(courseController.findAll(page)).thenReturn(new ArrayList<>()); - String response = mockMvc.perform(get("/courses/findAll").param("page", Integer.toString(page))) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - - assertThat("response", response, is("[]")); + Mockito.when(courseFacade.findAll((PageRequest) any())) + .thenReturn(new PageImpl<>(new ArrayList<>())); + String response = mockMvc.perform(get("/courses/findAll?page=0")) + .andExpect(status().is2xxSuccessful()).andReturn().getResponse().getContentAsString(); + assertTrue(response.contains("\"content\":[]")); } @Test void findAllWithoutPage() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt())).thenReturn(new ArrayList<>()); - mockMvc.perform(get("/courses/findAll")) + mockMvc.perform(get("/courses/findAll")) .andExpect(status().is4xxClientError()); } + @Test void findAllByLang() throws Exception { int page = 0; LanguageTypeDto lang = LanguageTypeDto.ENGLISH; - Mockito.when(courseController.findAll(page, lang)).thenReturn(new ArrayList<>()); + Mockito.when(courseFacade.findAll(lang)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/courses/findAllByLang") .param("page", Integer.toString(page)) .param("lang", lang.toString())) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - - assertThat("response", response, is("[]")); - } - - @Test - void findAllByLangWithoutParameters() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class))) - .thenReturn(new ArrayList<>()); - mockMvc.perform(get("/courses/findAllByLang")) - .andExpect(status().is4xxClientError()); - } + assertTrue(response.endsWith("[]")); } @Test void findAllByLangWithoutLang() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class))) - .thenReturn(new ArrayList<>()); - String page = "0"; - mockMvc.perform(get("/courses/findAllByLang").param("page", page)) + mockMvc.perform(get("/courses/findAllByLang")) .andExpect(status().is4xxClientError()); } @Test void findAllByLangProf() throws Exception { - int page = 0; LanguageTypeDto lang = LanguageTypeDto.ENGLISH; ProficiencyLevelDto proficiencyLevel = ProficiencyLevelDto.A1; - Mockito.when(courseController.findAll(page, lang, proficiencyLevel)).thenReturn(new ArrayList<>()); + Mockito.when(courseFacade.findAll(lang, proficiencyLevel)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/courses/findAllByLangProf") - .param("page", Integer.toString(page)) .param("lang", lang.toString()) .param("prof", proficiencyLevel.toString())) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - - assertThat("response", response, is("[]")); + assertTrue(response.endsWith("[]")); } @Test void findAllByLangProfWithoutParameters() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class), - ArgumentMatchers.isA(ProficiencyLevelDto.class))) - .thenReturn(new ArrayList<>()); mockMvc.perform(get("/courses/findAllByLangProf")) .andExpect(status().is4xxClientError()); } @Test void findAllByLangProfWithoutLangProf() throws Exception { - Mockito.when(courseController.findAll(ArgumentMatchers.anyInt(), - ArgumentMatchers.isA(LanguageTypeDto.class), - ArgumentMatchers.isA(ProficiencyLevelDto.class))) - .thenReturn(new ArrayList<>()); String page = "0"; mockMvc.perform(get("/courses/findAllByLangProf").param("page", page)) .andExpect(status().is4xxClientError()); @@ -183,106 +166,92 @@ public class CourseTest { @Test void updateCourse() throws Exception { - String id = "0"; - Mockito.when(courseController.update(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(CourseCreateDto.class))) + Long id = 0L; + Mockito.when(courseFacade.update(ArgumentMatchers.eq(id), + ArgumentMatchers.isA(CourseCreateDto.class))) .thenReturn(courseDto); mockMvc.perform(put("/courses/update/" + id) - .content(asJsonString(courseDto)) - .contentType(MediaType.APPLICATION_JSON)) + .content(asJsonString(courseDto)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.id").value(courseDto.getId())) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) + .andExpect(jsonPath("$.id").value(courseDto.getId())); } @Test void updateCourseWithoutParameter() throws Exception { - Mockito.when(courseController.update(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(CourseCreateDto.class))) - .thenReturn(courseDto); mockMvc.perform(put("/courses/update")) .andExpect(status().is4xxClientError()); } @Test void deleteCourse() throws Exception { - String id = "0"; - Mockito.doNothing().when(courseController).delete(id); + Long id = 0L; + Mockito.doNothing().when(courseFacade).delete(id); mockMvc.perform(delete("/courses/delete/" + id)) - .andExpect(status().isOk()); + .andExpect(status().is2xxSuccessful()); } @Test void deleteCourseWithoutParameter() throws Exception { - Mockito.doNothing().when(courseController).delete(ArgumentMatchers.anyString()); - mockMvc.perform(delete("/courses/delete/")) .andExpect(status().is4xxClientError()); } @Test void enrolCourse() throws Exception { - String id = "0"; - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + Long id = 0L; + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); + student.setId(1L); - CourseDto courseDtoWithStudent = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); - courseDtoWithStudent.setStudentIds(new ArrayList<>(List.of(student.getId()))); + CourseDto courseDtoWithStudent = new CourseDto("english b2 course", 10, LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2); + courseDtoWithStudent.setStudents(new ArrayList<>(List.of(student))); - Mockito.when(courseController.enrol(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(UserDto.class))).thenReturn(courseDtoWithStudent); + Mockito.when(courseFacade.enrol(ArgumentMatchers.anyLong(), + ArgumentMatchers.anyLong())).thenReturn(courseDtoWithStudent); - mockMvc.perform(patch("/courses/enrol/" + id) + mockMvc.perform(patch("/courses/enrol/" + id).param("studentId", String.valueOf(1L)) .content(asJsonString(student)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.studentIds").value(student.getId())) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")); } @Test void enrolCourseWithoutUserParameter() throws Exception { - String id = "0"; - Mockito.when(courseController.enrol(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(courseDto); - mockMvc.perform(patch("/courses/enrol/" + id)) + mockMvc.perform(patch("/courses/enrol/" + 0L)) .andExpect(status().is4xxClientError()); } @Test void enrolCourseWithoutCourseIdParameter() throws Exception { - Mockito.when(courseController.enrol(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); mockMvc.perform(patch("/courses/enrol/") - .content(asJsonString(student)) - .contentType(MediaType.APPLICATION_JSON)) + .content(asJsonString(student)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); } @Test void expelCourse() throws Exception { - String id = "0"; - Mockito.when(courseController.expel(ArgumentMatchers.eq(id), + Long id = 0L; + Mockito.when(courseFacade.expel(ArgumentMatchers.eq(id), ArgumentMatchers.isA(UserDto.class))) .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); mockMvc.perform(patch("/courses/expel/" + id) .content(asJsonString(student)) @@ -290,42 +259,25 @@ public class CourseTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("english b2 course")) .andExpect(jsonPath("$.capacity").value(10)) - .andExpect(jsonPath("$.languageTypeDto").value("ENGLISH")) - .andExpect(jsonPath("$.proficiencyLevelDto").value("B2")) - .andExpect(jsonPath("$.studentIds").isEmpty()) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.language").value("ENGLISH")) + .andExpect(jsonPath("$.proficiency").value("B2")) + .andExpect(jsonPath("$.students").isEmpty()); } @Test void expelCourseWithoutUserParameter() throws Exception { - String id = "0"; - Mockito.when(courseController.expel(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(courseDto); - - mockMvc.perform(patch("/courses/expel/" + id)) + mockMvc.perform(patch("/courses/expel/" + 0L)) .andExpect(status().is4xxClientError()); } @Test void deleteCourseWithoutCourseIdParameter() throws Exception { - Mockito.when(courseController.expel(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(courseDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); mockMvc.perform(patch("/courses/expel/") .content(asJsonString(student)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); } - - public static String asJsonString(final Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } } diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseFacadeTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e0653ae4ea65e4d524531dc187346e81a5b57ed0 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseFacadeTest.java @@ -0,0 +1,156 @@ +package org.fuseri.modulelanguageschool.course; + + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.junit.jupiter.api.Test; +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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SpringBootTest +@AutoConfigureMockMvc +final class CourseFacadeTest { + + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final CourseDto courseDto = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final CourseCreateDto courseCreateDto = new CourseCreateDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final Course course = new Course(); + private final User user = new User(); + private final List<Course> courseList = List.of(course); + private final List<CourseDto> courseDtoList = List.of(courseDto); + private final LanguageTypeDto languageTypeDto = LanguageTypeDto.ENGLISH; + private final ProficiencyLevelDto proficiencyLevelDto = ProficiencyLevelDto.B1; + + + @Autowired + private CourseFacade courseFacade; + + @MockBean + private CourseService courseService; + + @MockBean + private CourseMapper courseMapper; + + @MockBean + private UserMapper userMapper; + + @Test + void create() { + when(courseMapper.mapToCourse(courseCreateDto)).thenReturn(course); + when(courseService.save(course)).thenReturn(course); + when(courseMapper.mapToDto(course)).thenReturn(courseDto); + + CourseDto actualDto = courseFacade.create(courseCreateDto); + + assertEquals(courseDto, actualDto); + } + + @Test + void testFindById() { + Long id = 0L; + when(courseService.findById(id)).thenReturn(course); + when(courseMapper.mapToDto(course)).thenReturn(courseDto); + + CourseDto actualDto = courseFacade.findById(id); + + assertNotNull(actualDto); + assertEquals(courseDto, actualDto); + } + + @Test + void testFindAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<Course> certificatePage = new PageImpl<>(List.of(course), pageable, 0); + Page<CourseDto> expectedPageDto = new PageImpl<>(List.of(courseDto), pageable, 0); + + when(courseService.findAll(pageable)).thenReturn(certificatePage); + when(courseMapper.mapToPageDto(certificatePage)).thenReturn(expectedPageDto); + + Page<CourseDto> actualPageDto = courseFacade.findAll(pageable); + + assertEquals(expectedPageDto, actualPageDto); + } + + @Test + void update() { + Long id = 1L; + when(courseMapper.mapToCourse(courseCreateDto)).thenReturn(course); + when(courseService.update(id, course)).thenReturn(course); + when(courseMapper.mapToDto(course)).thenReturn(courseDto); + + CourseDto actualDto = courseFacade.update(id, courseCreateDto); + + assertEquals(courseDto, actualDto); + } + + @Test + void testDelete() { + Long id = 1L; + courseFacade.delete(id); + verify(courseService).delete(id); + } + + @Test + void testFindAllByLanguage() { + when(courseService.findAll(any(Language.class))).thenReturn(courseList); + when(courseMapper.mapToList(courseList)).thenReturn(courseDtoList); + + List<CourseDto> actualDtoList = courseFacade.findAll(languageTypeDto); + + assertNotNull(actualDtoList); + assertEquals(courseDtoList, actualDtoList); + } + + @Test + void testFindAllByLanguageAndProf() { + when(courseService.findAll(any(Language.class), any(ProficiencyLevel.class))).thenReturn(courseList); + when(courseMapper.mapToList(courseList)).thenReturn(courseDtoList); + + List<CourseDto> actualDtoList = courseFacade.findAll(languageTypeDto, proficiencyLevelDto); + + assertNotNull(actualDtoList); + assertEquals(courseDtoList, actualDtoList); + } + + @Test + void testExpel() { + Long id = 0L; + when(courseMapper.mapToDto(course)).thenReturn(courseDto); + when(userMapper.fromDto(USER)).thenReturn(user); + when(courseService.expel(anyLong(), any(User.class))).thenReturn(course); + + CourseDto actualDto = courseFacade.expel(id, USER); + + assertNotNull(actualDto); + assertEquals(courseDto, actualDto); + } + + + +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseMapperTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8e87376603461ab9eb8362f365d0cdcd73659862 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseMapperTest.java @@ -0,0 +1,123 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.model.dto.course.CourseCreateDto; +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.Collections; +import java.util.List; + +@SpringBootTest +final class CourseMapperTest { + + private final CourseDto courseDto = new CourseDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final CourseCreateDto courseCreateDto = new CourseCreateDto("AJ1", 10, + LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1); + @Autowired + private CourseMapper CourseMapper; + + @BeforeEach + void setUp() + { + course.setId(1L); + courseDto.setId(1L); + } + + @Test + void mapNullToDto() { + var createdDto = CourseMapper.mapToDto(null); + + Assertions.assertNull(createdDto); + } + + @Test + void mapToDto() { + var createdDto = CourseMapper.mapToDto(course); + + Assertions.assertNotNull(createdDto); + } + + @Test + void mapNullToCourseCourseDto() { + var createdCourse = CourseMapper.mapToCourse((CourseDto) null); + + Assertions.assertNull(createdCourse); + } + + @Test + void mapToCourseCourseDto() { + var createdCourse = CourseMapper.mapToCourse(courseDto); + + Assertions.assertNotNull(createdCourse); + } + + @Test + void mapNullToList() { + var courseDtos = CourseMapper.mapToList(null); + + Assertions.assertNull(courseDtos); + } + + @Test + void mapToEmptyList() { + var courseDtos = CourseMapper.mapToList(Collections.emptyList()); + + Assertions.assertEquals(courseDtos.size(), 0); + } + + @Test + void mapToList() { + var courseDtos = CourseMapper.mapToList(Collections.singletonList(course)); + + Assertions.assertEquals(1, courseDtos.size()); + } + + @Test + void mapToEmptyPageDto() { + Page<CourseDto> pageDto = CourseMapper.mapToPageDto(Page.empty()); + + Assertions.assertEquals(1, pageDto.getTotalPages()); + } + + @Test + void mapToPageDto() { + List<Course> courses = List.of(course); + Page<Course> page = new PageImpl<>(courses, PageRequest.of(0, 1), courses.size()); + Page<CourseDto> pageDto = CourseMapper.mapToPageDto(page); + + Assertions.assertEquals(page.getTotalPages(), pageDto.getTotalPages()); + Assertions.assertEquals(page.getNumber(), pageDto.getNumber()); + Assertions.assertEquals(page.getNumberOfElements(), pageDto.getNumberOfElements()); + Assertions.assertEquals(page.getSize(), pageDto.getSize()); + Assertions.assertEquals(page.getTotalElements(), pageDto.getTotalElements()); + +// Assertions.assertEquals(courseDto, pageDto.getContent().get(0)); + } + + @Test + void mapNullToCourseCourseCreateDto() { + var createdCourse = CourseMapper.mapToCourse((CourseCreateDto) null); + + Assertions.assertNull(createdCourse); + } + + @Test + void mapToCourseCourseCreateDto() { + var createdCourse = CourseMapper.mapToCourse(courseCreateDto); + + Assertions.assertEquals(course, createdCourse); + } + +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseRepositoryTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4ca1c8fa74f4376a99f7e87e704665835b6c3eca --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseRepositoryTest.java @@ -0,0 +1,92 @@ +package org.fuseri.modulelanguageschool.course; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@DataJpaTest +class CourseRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private CourseRepository courseRepository; + + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1); + + @Test + void saveCourse() { + Course saved = courseRepository.save(course); + + Assertions.assertNotNull(saved); + Assertions.assertEquals(course, saved); + } + + @Test + void findById() { + entityManager.persist(course); + entityManager.flush(); + + Course found = courseRepository.findById(course.getId()).orElse(null); + + Assertions.assertNotNull(found); + Assertions.assertEquals(found, course); + } + + + @Test + void findAllByLang() { + entityManager.persist(course); + entityManager.flush(); + + List<Course> found = courseRepository.findAllByLang(course.getLanguage()); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), course); + } + + @Test + void findAllByLangProf() { + entityManager.persist(course); + entityManager.flush(); + + List<Course> found = courseRepository.findAllByLangProf(course.getLanguage(), course.getProficiency()); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), course); + } + @Test + public void testFindAllCourses() { + Course course1 = new Course(); + Course course2 = new Course(); + + courseRepository.save(course1); + courseRepository.save(course2); + + Page<Course> coursePage = courseRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, coursePage.getTotalElements()); + Assertions.assertEquals(coursePage.getContent(), Arrays.asList(course1, course2)); + } + + @Test + public void testDeleteCourse() { + Long courseId = entityManager.persist(new Course()).getId(); + entityManager.flush(); + + courseRepository.deleteById(courseId); + + Assertions.assertTrue(courseRepository.findById(courseId).isEmpty()); + } + +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseServiceTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5b952e05332fc5c8b49abb22479e4d131bc5a845 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/course/CourseServiceTest.java @@ -0,0 +1,175 @@ +package org.fuseri.modulelanguageschool.course; + +import org.fuseri.modulelanguageschool.user.Address; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.server.ResponseStatusException; + +import java.util.*; + +import static org.mockito.Mockito.*; + +@SpringBootTest +final class CourseServiceTest { + + @MockBean + private CourseRepository courseRepository; + + @Autowired + private CourseService courseService; + + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1, new ArrayList<>(), false); + + private final User user = new User("novakovat", UserType.STUDENT, + "novakova@gamil.com", "password", "Tereza", + "Nováková", new Address(), new HashSet<>(), new HashMap<>()); + private final Course courseWithEnrolledStudent = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1, new ArrayList<>(List.of(user)), false); + private final List<Course> courses = List.of(course, course); + + @Test + void save() { + when(courseRepository.save(course)).thenReturn(course); + + Course result = courseService.save(course); + + Assertions.assertEquals(course, result); + verify(courseRepository).save(course); + } + + @Test + void notFoundById() { + when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> courseService.findById(anyLong())); + } + + @Test + void findById() { + when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); + + Course result = courseService.findById(anyLong()); + + Assertions.assertEquals(course, result); + verify(courseRepository).findById(anyLong()); + } + + @Test + void findAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<Course> page = new PageImpl<>(Collections.emptyList(), pageable, 0); + + when(courseRepository.findAll(pageable)).thenReturn(page); + + Page<Course> result = courseService.findAll(pageable); + + Assertions.assertEquals(page, result); + verify(courseRepository).findAll(pageable); + } + + @Test + void update() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); + when(courseRepository.save(course)).thenReturn(course); + + Course result = courseService.update(id, course); + + Assertions.assertEquals(course, result); + verify(courseRepository).findById(anyLong()); + verify(courseRepository).save(course); + } + + @Test + void updateIdDoesNotExist() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> courseService.update(id, course)); + } + + @Test + void delete() { + Long id = 1L; + courseService.delete(id); + + verify(courseRepository).deleteById(id); + } + + @Test + void findAllByLanguage() { + Language language = Language.ENGLISH; + when(courseRepository.findAllByLang(any(Language.class))).thenReturn(courses); + + List<Course> result = courseService.findAll(language); + + Assertions.assertEquals(courses, result); + verify(courseRepository).findAllByLang(language); + } + + @Test + void findAllByLanguageAndProf() { + Language language = Language.ENGLISH; + ProficiencyLevel proficiencyLevel = ProficiencyLevel.B1; + + when(courseRepository.findAllByLangProf(any(Language.class), any(ProficiencyLevel.class))).thenReturn(courses); + + List<Course> result = courseService.findAll(language, proficiencyLevel); + + Assertions.assertEquals(courses, result); + verify(courseRepository).findAllByLangProf(language, proficiencyLevel); + } + + @Test + void enrol() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); + when(courseRepository.save(any(Course.class))).thenReturn(courseWithEnrolledStudent); + + Course result = courseService.enrol(id, user); + + Assertions.assertEquals(courseWithEnrolledStudent, result); + verify(courseRepository).findById(id); + verify(courseRepository).save(any(Course.class)); + } + + @Test + void enrolIdDoesNotExist() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> courseService.enrol(id, user)); + } + + @Test + void expel() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.of(courseWithEnrolledStudent)); + when(courseRepository.save(any(Course.class))).thenReturn(course); + + Course result = courseService.expel(id, user); + + Assertions.assertEquals(course, result); + verify(courseRepository).findById(id); + verify(courseRepository).save(any(Course.class)); + } + + @Test + void expelIdDoesNotExist() { + Long id = 1L; + when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> courseService.expel(id, user)); + } + +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureControllerTest.java similarity index 59% rename from application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java rename to application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureControllerTest.java index e254fd91b028d5636361c47afd12a0c5483ac2ab..a6177a7f2bf64e84a3f3e78926a0ad34fe79e8f9 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureControllerTest.java @@ -6,6 +6,7 @@ import org.fuseri.model.dto.lecture.LectureCreateDto; import org.fuseri.model.dto.lecture.LectureDto; import org.fuseri.model.dto.user.AddressDto; import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -15,58 +16,71 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; + import java.time.LocalDateTime; import java.util.ArrayList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc -public class LectureTest { +public class LectureControllerTest { + + private final LocalDateTime now = LocalDateTime.now(); + private final LectureCreateDto lectureCreateDto = new LectureCreateDto( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, 0L); + private final LectureDto lectureDto = new LectureDto( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, 0L, 0L, Collections.emptyList()); + @Autowired ObjectMapper objectMapper; + @Autowired private MockMvc mockMvc; @MockBean - private LectureController lectureController; + private LectureFacade lectureFacade; - private final LectureCreateDto lectureCreateDto = new LectureCreateDto( - LocalDateTime.now().plusDays(2), - LocalDateTime.now().plusDays(2).plusHours(2), - "Learning how to spell deprecated", - 10, "0"); - private final LectureDto lectureDto = new LectureDto( - LocalDateTime.now().plusDays(2), - LocalDateTime.now().plusDays(2).plusHours(2), - "Learning how to spell deprecated", - 10, "0", "0"); + public static String asJsonString(final Object obj) { + try { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } @Test void createLecture() throws Exception { - Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create") + Mockito.when(lectureFacade.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); + mockMvc.perform(post("/lectures") .content(asJsonString(lectureCreateDto)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void createInvalidLecture() throws Exception { LectureCreateDto invalidLectureCreateDto = new LectureCreateDto(null, null, null, null, null); - Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create") + mockMvc.perform(post("/lectures") .content(asJsonString(invalidLectureCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().is4xxClientError()); @@ -74,193 +88,161 @@ public class LectureTest { @Test void createLectureWithoutParameter() throws Exception { - Mockito.when(lectureController.create(ArgumentMatchers.isA(LectureCreateDto.class))).thenReturn(lectureDto); - mockMvc.perform(post("/lectures/create")) + mockMvc.perform(post("/lectures")) .andExpect(status().is4xxClientError()); } @Test void findLecture() throws Exception { - String id = "0"; - Mockito.when(lectureController.find(id)).thenReturn(lectureDto); + Long id = 0L; + Mockito.when(lectureFacade.findById(id)).thenReturn(lectureDto); mockMvc.perform(get("/lectures/find/" + id)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void findLectureWithoutId() throws Exception { - Mockito.when(lectureController.find(ArgumentMatchers.anyString())).thenReturn(lectureDto); mockMvc.perform(get("/lectures/find/")) .andExpect(status().is4xxClientError()); } @Test void findLecturesByCourse() throws Exception { - String id = "0"; - Mockito.when(lectureController.findByCourse(id)).thenReturn(new ArrayList<>()); + Long id = 0L; + Mockito.when(lectureFacade.findAll(id)).thenReturn(new ArrayList<>()); String response = mockMvc.perform(get("/lectures/findByCourse") - .param("courseId", id)) + .param("courseId", String.valueOf(id))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - - assertThat("response", response, is("[]")); + assertTrue(response.endsWith("[]")); } @Test void findLecturesByCourseWithoutParameter() throws Exception { - Mockito.when(lectureController.findByCourse(ArgumentMatchers.anyString())).thenReturn(new ArrayList<>()); mockMvc.perform(get("/lectures/findByCourse")) .andExpect(status().is4xxClientError()); } @Test void updateLecture() throws Exception { - String id = "0"; - Mockito.when(lectureController.update(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(LectureCreateDto.class))) + Long id = 0L; + Mockito.when(lectureFacade.update(ArgumentMatchers.eq(id), + ArgumentMatchers.isA(LectureCreateDto.class))) .thenReturn(lectureDto); mockMvc.perform(put("/lectures/update/" + id) .content(asJsonString(lectureCreateDto)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void updateLectureWithoutParameter() throws Exception { - Mockito.when(lectureController. - update(ArgumentMatchers.anyString(), ArgumentMatchers.isA(LectureCreateDto.class))) - .thenReturn(lectureDto); mockMvc.perform(put("/lectures/update")) .andExpect(status().is4xxClientError()); } @Test void deleteLecture() throws Exception { - String id = "0"; - Mockito.doNothing().when(lectureController).delete(id); + Long id = 0L; + Mockito.doNothing().when(lectureFacade).delete(id); mockMvc.perform(delete("/lectures/delete/" + id)) - .andExpect(status().isOk()); + .andExpect(status().is2xxSuccessful()); } @Test void deleteCourseWithoutParameter() throws Exception { - Mockito.doNothing().when(lectureController).delete(ArgumentMatchers.anyString()); mockMvc.perform(delete("/lectures/delete/")) .andExpect(status().is4xxClientError()); } @Test void setLecturerForLecture() throws Exception { - String id = "0"; - Mockito.when(lectureController.setLecturer(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(UserDto.class))) + Long id = 0L; + Mockito.when(lectureFacade.setLecturer(ArgumentMatchers.eq(id), + ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); mockMvc.perform(patch("/lectures/setLecturer/" + id) .content(asJsonString(student)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void setLecturerForLectureWithoutParameters() throws Exception { - Mockito.when(lectureController.setLecturer(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/setLecturer")) .andExpect(status().is4xxClientError()); } @Test void enrolLecture() throws Exception { - String id = "0"; - Mockito.when(lectureController.enrol(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(UserDto.class))) + Long id = 0L; + Mockito.when(lectureFacade.enrol(ArgumentMatchers.eq(id), + ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(),UserType.STUDENT ); mockMvc.perform(patch("/lectures/enrol/" + id) .content(asJsonString(student)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void enrolCourseWithoutUserParameters() throws Exception { - Mockito.when(lectureController.enrol(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/enrol")) .andExpect(status().is4xxClientError()); } @Test void expelLecture() throws Exception { - String id = "0"; - Mockito.when(lectureController.expel(ArgumentMatchers.eq(id), - ArgumentMatchers.isA(UserDto.class))) + Long id = 0L; + Mockito.when(lectureFacade.expel(ArgumentMatchers.eq(id), + ArgumentMatchers.isA(UserDto.class))) .thenReturn(lectureDto); - UserDto student = new UserDto("novakovat","novakova@gamil.com", "Tereza", - "Nováková", new AddressDto()); + UserDto student = new UserDto("novakovat", "novakova@gamil.com", "Tereza", + "Nováková", new AddressDto(), UserType.STUDENT); mockMvc.perform(patch("/lectures/expel/" + id) .content(asJsonString(student)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.from").isNotEmpty()) - .andExpect(jsonPath("$.to").isNotEmpty()) + .andExpect(jsonPath("$.lectureFrom").isNotEmpty()) + .andExpect(jsonPath("$.lectureTo").isNotEmpty()) .andExpect(jsonPath("$.topic").value("Learning how to spell deprecated")) .andExpect(jsonPath("$.capacity").value("10")) .andExpect(jsonPath("$.lecturerId").value("0")) - .andExpect(jsonPath("$.courseId").value("0")) - .andReturn().getResponse().getContentAsString(); + .andExpect(jsonPath("$.courseId").value("0")); } @Test void expelCourseWithoutUserParameters() throws Exception { - Mockito.when(lectureController.expel(ArgumentMatchers.anyString(), - ArgumentMatchers.isA(UserDto.class))) - .thenReturn(lectureDto); mockMvc.perform(patch("/lectures/expel")) .andExpect(status().is4xxClientError()); } - public static String asJsonString(final Object obj) { - try { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - return mapper.writeValueAsString(obj); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureFacadeTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e970e966e1caf8b314bfd13f8580693eaddd6613 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureFacadeTest.java @@ -0,0 +1,187 @@ +package org.fuseri.modulelanguageschool.lecture; + + +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.fuseri.modulelanguageschool.course.CourseService; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserMapper; +import org.junit.jupiter.api.Test; +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.boot.test.mock.mockito.MockBean; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SpringBootTest +@AutoConfigureMockMvc +final class LectureFacadeTest { + + private final UserDto USER = new UserDto("novakovat", + "novakova@gamil.com", "Tereza", "Nováková", new AddressDto(), UserType.STUDENT); + private final LectureCreateDto lectureCreateDto = new LectureCreateDto( + LocalDateTime.now().plusDays(2), + LocalDateTime.now().plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, 0L); + private final LectureDto lectureDto = new LectureDto( + LocalDateTime.now().plusDays(2), + LocalDateTime.now().plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, 0L, 0L, Collections.emptyList()); + private final Lecture lecture = new Lecture(); + private final User user = new User(); + private final List<Lecture> lectureList = List.of(lecture); + private final List<LectureDto> lectureDtoList = List.of(lectureDto); + private final LanguageTypeDto languageTypeDto = LanguageTypeDto.ENGLISH; + private final ProficiencyLevelDto proficiencyLevelDto = ProficiencyLevelDto.B1; + + + @Autowired + private LectureFacade lectureFacade; + + @MockBean + private LectureService lectureService; + + @MockBean + private LectureMapper lectureMapper; + + @MockBean + private UserMapper userMapper; + + @Autowired + private CourseService courseService; + + @Test + void create() { + when(lectureMapper.mapToLecture(lectureCreateDto, courseService)).thenReturn(lecture); + when(lectureService.save(lecture)).thenReturn(lecture); + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + + LectureDto actualDto = lectureFacade.create(lectureCreateDto); + + assertEquals(lectureDto, actualDto); + } + + @Test + void testFindById() { + Long id = 0L; + when(lectureService.findById(id)).thenReturn(lecture); + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + + LectureDto actualDto = lectureFacade.findById(id); + + assertNotNull(actualDto); + assertEquals(lectureDto, actualDto); + } + + @Test + void testFindAll() { + var id = 1L; + when(lectureService.findAllByCourse(anyLong())).thenReturn(lectureList); + when(lectureMapper.mapToList(lectureList)).thenReturn(lectureDtoList); + + List<LectureDto> actualPageDtos = lectureFacade.findAll(id); + + assertEquals(lectureDtoList, actualPageDtos); + } + + @Test + void update() { + Long id = 1L; + when(lectureMapper.mapToLecture(lectureCreateDto, courseService)).thenReturn(lecture); + when(lectureService.update(id, lecture)).thenReturn(lecture); + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + + LectureDto actualDto = lectureFacade.update(id, lectureCreateDto); + + assertEquals(lectureDto, actualDto); + } + + @Test + void testDelete() { + Long id = 1L; + lectureFacade.delete(id); + verify(lectureService).delete(id); + } + + @Test + void testFindAllByLanguage() { + when(lectureService.findAll(any(Language.class))).thenReturn(lectureList); + when(lectureMapper.mapToList(lectureList)).thenReturn(lectureDtoList); + + List<LectureDto> actualDtoList = lectureFacade.findAll(languageTypeDto); + + assertNotNull(actualDtoList); + assertEquals(lectureDtoList, actualDtoList); + } + + @Test + void testFindAllByLanguageAndProf() { + when(lectureService.findAll(any(Language.class), any(ProficiencyLevel.class))).thenReturn(lectureList); + when(lectureMapper.mapToList(lectureList)).thenReturn(lectureDtoList); + + List<LectureDto> actualDtoList = lectureFacade.findAll(languageTypeDto, proficiencyLevelDto); + + assertNotNull(actualDtoList); + assertEquals(lectureDtoList, actualDtoList); + } + + @Test + void testEnrol() { + Long id = 0L; + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + when(userMapper.fromDto(USER)).thenReturn(user); + when(lectureService.enrol(anyLong(), any(User.class))).thenReturn(lecture); + + LectureDto actualDto = lectureFacade.enrol(id, USER); + + assertNotNull(actualDto); + assertEquals(lectureDto, actualDto); + } + + @Test + void testExpel() { + Long id = 0L; + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + when(userMapper.fromDto(USER)).thenReturn(user); + when(lectureService.expel(anyLong(), any(User.class))).thenReturn(lecture); + + LectureDto actualDto = lectureFacade.expel(id, USER); + + assertNotNull(actualDto); + assertEquals(lectureDto, actualDto); + } + + @Test + void testSetLecturer() { + Long id = 0L; + when(lectureMapper.mapToDto(lecture)).thenReturn(lectureDto); + when(userMapper.fromDto(USER)).thenReturn(user); + when(lectureService.setLecturer(anyLong(), any(User.class))).thenReturn(lecture); + + LectureDto actualDto = lectureFacade.setLecturer(id, USER); + + assertNotNull(actualDto); + assertEquals(lectureDto, actualDto); + } +} + diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureMapperTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2436becb834430514a1e2cc33d130428fce8b6c9 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureMapperTest.java @@ -0,0 +1,146 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.model.dto.lecture.LectureCreateDto; +import org.fuseri.model.dto.lecture.LectureDto; +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.CourseService; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.Address; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserService; +import org.fuseri.modulelanguageschool.user.UserType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@SpringBootTest +final class LectureMapperTest { + + private final LocalDateTime now = LocalDateTime.now(); + + private final User lecturer = new User("dolezelt", UserType.LECTURER, + "dolezel@gmail.com", "password", "Tomáš", + "DoleĹľel", new Address(), new HashSet<>(), new HashMap<>()); + private final User student = new User("novakovat", UserType.STUDENT, + "novakova@gmail.com", "password", "Tereza", + "Nováková", new Address(), new HashSet<>(), new HashMap<>()); + + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1); + + private final LectureCreateDto lectureCreateDto = new LectureCreateDto( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, course.getId()); + private final LectureDto lectureDto = new LectureDto( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, lecturer.getId(), course.getId(), Collections.emptyList()); + private final Lecture newLecture = new Lecture( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, + course, null, new ArrayList<>(List.of())); + + private final Lecture lecture = new Lecture( + now.plusDays(2), + now.plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, + course, lecturer, new ArrayList<>(List.of())); + + @Autowired + private org.fuseri.modulelanguageschool.lecture.LectureMapper LectureMapper; + + @MockBean + private CourseService courseService; + + @MockBean + private UserService userService; + + @Test + void mapNullToDto() { + var createdDto = LectureMapper.mapToDto(null); + + Assertions.assertNull(createdDto); + } + + @Test + void mapToDto() { + var createdDto = LectureMapper.mapToDto(lecture); + + Assertions.assertEquals(lectureDto, createdDto); + } + + @Test + void mapNullToLectureDto() { + var createdLecture = LectureMapper.mapToLecture((LectureDto) null, courseService, userService); + + Assertions.assertNull(createdLecture); + } + + @Test + void mapToLectureDto() { + when(courseService.findById(any())).thenReturn(course); + when(userService.find(any())).thenReturn(lecturer); + + var createdLecture = LectureMapper.mapToLecture(lectureDto, courseService, userService); + Assertions.assertEquals(lecture, createdLecture); + } + + @Test + void mapNullToList() { + var lectureDtos = LectureMapper.mapToList(null); + + Assertions.assertNull(lectureDtos); + } + + @Test + void mapToEmptyList() { + var lectureDtos = LectureMapper.mapToList(Collections.emptyList()); + + Assertions.assertEquals(lectureDtos.size(), 0); + } + + @Test + void mapToList() { + when(courseService.findById(anyLong())).thenReturn(course); + when(userService.find(anyLong())).thenReturn(lecturer); + + var lectureDtos = LectureMapper.mapToList(Collections.singletonList(lecture)); + + Assertions.assertEquals(1, lectureDtos.size()); + Assertions.assertEquals(lectureDto, lectureDtos.get(0)); + } + + @Test + void mapNullToLectureLectureCreateDto() { + var createdLecture = LectureMapper.mapToLecture((LectureCreateDto) null, courseService); + + Assertions.assertNull(createdLecture); + } + + @Test + void mapToLectureLectureCreateDto() { + when(courseService.findById(any())).thenReturn(course); + + var createdLecture = LectureMapper.mapToLecture(lectureCreateDto, courseService); + + Assertions.assertEquals(newLecture, createdLecture); + } + +} + diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureRepositoryTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2991a492c633b26b6f309504be2221ea3dc454db --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureRepositoryTest.java @@ -0,0 +1,133 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.Address; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.time.LocalDateTime; +import java.util.*; + +@DataJpaTest +class LectureRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private LectureRepository lectureRepository; + + + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1); + private final User student = new User("novakovat", UserType.STUDENT, + "novakova@gmail.com", "password", "Tereza", + "Nováková", new Address(), new HashSet<>(), new HashMap<>()); + private final User lecturer = new User("dolezelt", UserType.LECTURER, + "dolezel@gmail.com", "password", "Tomáš", + "DoleĹľel", new Address(), new HashSet<>(), new HashMap<>()); + private final Lecture lecture = new Lecture( + LocalDateTime.now().plusDays(2), + LocalDateTime.now().plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, + course, lecturer, new ArrayList<>(List.of(student))); + + @Test + void saveLecture() { + Lecture saved = lectureRepository.save(lecture); + + Assertions.assertNotNull(saved); + Assertions.assertEquals(lecture, saved); + } + + @Test + void findById() { + entityManager.persist(lecturer); + entityManager.persist(student); + entityManager.persist(course); + entityManager.persist(lecture); + entityManager.flush(); + + Lecture found = lectureRepository.findById(lecture.getId()).orElse(null); + + Assertions.assertNotNull(found); + Assertions.assertEquals(found, lecture); + } + + @Test + void findAllByCourse() { + entityManager.persist(lecturer); + entityManager.persist(student); + entityManager.persist(course); + entityManager.persist(lecture); + entityManager.flush(); + + List<Lecture> found = lectureRepository.findAllByCourse(lecture.getCourse().getId()); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), lecture); + } + + @Test + void findAllByLang() { + entityManager.persist(lecturer); + entityManager.persist(student); + entityManager.persist(course); + entityManager.persist(lecture); + entityManager.flush(); + + List<Lecture> found = lectureRepository.findAllByLang(lecture.getCourse().getLanguage()); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), lecture); + } + + @Test + void findAllByLangProf() { + entityManager.persist(lecturer); + entityManager.persist(student); + entityManager.persist(course); + entityManager.persist(lecture); + entityManager.flush(); + + List<Lecture> found = lectureRepository.findAllByLangProf(lecture.getCourse().getLanguage(), + lecture.getCourse().getProficiency()); + + Assertions.assertEquals(1, found.size()); + Assertions.assertEquals(found.get(0), lecture); + } + @Test + void testFindAllLectures() { + Lecture lecture1 = new Lecture(); + Lecture lecture2 = new Lecture(); + + lectureRepository.save(lecture1); + lectureRepository.save(lecture2); + + Page<Lecture> lecturePage = lectureRepository.findAll(PageRequest.of(0, 42)); + + Assertions.assertEquals(2, lecturePage.getTotalElements()); + Assertions.assertEquals(lecturePage.getContent(), Arrays.asList(lecture1, lecture2)); + } + + @Test + void testDeleteLecture() { + Long lectureId = entityManager.persist(new Lecture()).getId(); + entityManager.flush(); + + lectureRepository.deleteById(lectureId); + + Assertions.assertTrue(lectureRepository.findById(lectureId).isEmpty()); + } +} + diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureServiceTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..814466963f667f324f1c84ff3ed46f3b4646d6a7 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/lecture/LectureServiceTest.java @@ -0,0 +1,179 @@ +package org.fuseri.modulelanguageschool.lecture; + +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.fuseri.modulelanguageschool.user.Address; +import org.fuseri.modulelanguageschool.user.User; +import org.fuseri.modulelanguageschool.user.UserType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.mockito.Mockito.*; + +@SpringBootTest +final class LectureServiceTest { + + @MockBean + private LectureRepository lectureRepository; + + @Autowired + private LectureService lectureService; + + private final Course course = new Course("AJ1", 10, + Language.ENGLISH, ProficiencyLevel.A1); + private final User student = new User("novakovat", UserType.STUDENT, + "novakova@gmail.com", "password", "Tereza", + "Nováková", new Address(), new HashSet<>(), new HashMap<>()); + private final User lecturer = new User("dolezelt", UserType.LECTURER, + "dolezel@gmail.com", "password", "Tomáš", + "DoleĹľel", new Address(), new HashSet<>(), new HashMap<>()); + private final Lecture lecture = new Lecture( + LocalDateTime.now().plusDays(2), + LocalDateTime.now().plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, + course, lecturer, new ArrayList<>(List.of())); + + private final User user = new User("novakovat", UserType.STUDENT, + "novakova@gamil.com", "password", "Tereza", + "Nováková", new Address(), new HashSet<>(), new HashMap<>()); + + private final Lecture lectureWithEnrolledStudent = new Lecture( + LocalDateTime.now().plusDays(2), + LocalDateTime.now().plusDays(2).plusHours(2), + "Learning how to spell deprecated", + 10, + course, lecturer, new ArrayList<>(List.of(student))); + private final List<Lecture> lectures = new ArrayList<>(List.of(lecture, lecture)); + + @Test + void save() { + when(lectureRepository.save(lecture)).thenReturn(lecture); + + Lecture result = lectureService.save(lecture); + + Assertions.assertEquals(lecture, result); + verify(lectureRepository).save(lecture); + } + + @Test + void notFoundById() { + when(lectureRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> lectureService.findById(anyLong())); + + } + + @Test + void findById() { + when(lectureRepository.findById(anyLong())).thenReturn(Optional.of(lecture)); + + Lecture result = lectureService.findById(anyLong()); + + Assertions.assertEquals(lecture, result); + verify(lectureRepository).findById(anyLong()); + } + + @Test + void update() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.of(lecture)); + when(lectureRepository.save(lecture)).thenReturn(lecture); + + Lecture result = lectureService.update(id, lecture); + + Assertions.assertEquals(lecture, result); + verify(lectureRepository).findById(anyLong()); + verify(lectureRepository).save(lecture); + } + + @Test + void updateIdDoesNotExist() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> lectureService.update(id, lecture)); + } + + @Test + void delete() { + Long id = 1L; + lectureService.delete(id); + + verify(lectureRepository).deleteById(id); + } + + @Test + void findAllByLanguage() { + Language language = Language.ENGLISH; + when(lectureRepository.findAllByLang(any(Language.class))).thenReturn(lectures); + + List<Lecture> result = lectureService.findAll(language); + + Assertions.assertEquals(lectures, result); + verify(lectureRepository).findAllByLang(language); + } + + @Test + void findAllByLanguageAndProf() { + Language language = Language.ENGLISH; + ProficiencyLevel proficiencyLevel = ProficiencyLevel.B1; + + when(lectureRepository.findAllByLangProf(any(Language.class), any(ProficiencyLevel.class))).thenReturn(lectures); + + List<Lecture> result = lectureService.findAll(language, proficiencyLevel); + + Assertions.assertEquals(lectures, result); + verify(lectureRepository).findAllByLangProf(language, proficiencyLevel); + } + + @Test + void enrol() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.of(lecture)); + when(lectureRepository.save(any(Lecture.class))).thenReturn(lectureWithEnrolledStudent); + + Lecture result = lectureService.enrol(id, user); + + Assertions.assertEquals(lectureWithEnrolledStudent, result); + verify(lectureRepository).findById(id); + verify(lectureRepository).save(any(Lecture.class)); + } + + @Test + void enrolIdDoesNotExist() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> lectureService.enrol(id, user)); + } + + @Test + void expel() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.of(lectureWithEnrolledStudent)); + when(lectureRepository.save(any(Lecture.class))).thenReturn(lecture); + + Lecture result = lectureService.expel(id, user); + + Assertions.assertEquals(lecture, result); + verify(lectureRepository).findById(id); + verify(lectureRepository).save(any(Lecture.class)); + } + + @Test + void expelIdDoesNotExist() { + Long id = 1L; + when(lectureRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Assertions.assertThrows(ResponseStatusException.class, () -> lectureService.expel(id, user)); + } +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java index 85aa5f33c134528abba0f729717a020278e440bb..56633ad9bf451c08381d7e0eac0ef4887ba908e1 100644 --- a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java @@ -2,22 +2,36 @@ package org.fuseri.modulelanguageschool.user; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuseri.model.dto.course.CourseDto; import org.fuseri.model.dto.course.LanguageTypeDto; import org.fuseri.model.dto.course.ProficiencyLevelDto; -import org.fuseri.model.dto.user.*; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserAddLanguageDto; +import org.fuseri.model.dto.user.UserCreateDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserLoginDto; +import org.fuseri.model.dto.user.UserType; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; 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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Stream; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -32,9 +46,15 @@ class UserControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private UserFacade userFacade; + private static final AddressDto ADDRESS_TO_CREATE = new AddressDto( "Czechia", "Brno", "Masarykova", "45", "90033"); + private static final Map<LanguageTypeDto, ProficiencyLevelDto> languageProficiency = + Map.of(LanguageTypeDto.ENGLISH, ProficiencyLevelDto.A1); + private static final List<AddressDto> INVALID_ADDRESSES = List.of( new AddressDto("", "Brno", "Masarykova", "45", "90033"), new AddressDto("Czechia", "", "Masarykova", "45", "90033"), @@ -47,43 +67,52 @@ class UserControllerTest { new AddressDto("Czechia", "Brno", "Masarykova", null, "90033") ); - private static final UserCreateDto USER_TO_CREATE = new UserCreateDto( + private final UserCreateDto USER_CREATE_DTO = new UserCreateDto( "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE); + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency); - private static final UserLoginDto USER_TO_LOGIN = new UserLoginDto( + private static final UserLoginDto USER_LOGIN_DTO = new UserLoginDto( "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af"); + private final UserDto USER_DTO = new UserDto( + "xnovak", "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT); + private static Stream<UserCreateDto> invalidUsers() { var invalidUsers = Stream.of( new UserCreateDto("", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "", - "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "", "Peter", "Novak", ADDRESS_TO_CREATE), + "", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "", "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", "", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "", ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", "", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto(null, "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", null, - "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - null, "Peter", "Novak", ADDRESS_TO_CREATE), + null, "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", null, "Novak", ADDRESS_TO_CREATE), + "xnovak@emample.com", null, "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", null, ADDRESS_TO_CREATE), + "xnovak@emample.com", "Peter", null, ADDRESS_TO_CREATE, UserType.STUDENT, languageProficiency), new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "Novak", null) + "xnovak@emample.com", "Peter", "Novak", null, UserType.STUDENT, languageProficiency), + new UserCreateDto( + "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, null, languageProficiency), + new UserCreateDto( + "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", + "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE, UserType.STUDENT, null) ); var invalidAddressUsers = new ArrayList<UserCreateDto>(); for (var invalidAddress : INVALID_ADDRESSES) { invalidAddressUsers.add(new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", - "xnovak@emample.com", "Peter", "Novak", invalidAddress)); + "xnovak@emample.com", "Peter", "Novak", invalidAddress, UserType.STUDENT, languageProficiency)); } return Stream.concat(invalidUsers, invalidAddressUsers.stream()); @@ -98,16 +127,18 @@ class UserControllerTest { } @Test - void create() throws Exception { + void createUser() throws Exception { + Mockito.when(userFacade.create(ArgumentMatchers.isA(UserCreateDto.class))).thenReturn(USER_DTO); mockMvc.perform(post("/users") - .content(asJsonString(USER_TO_CREATE)) + .content(asJsonString(USER_CREATE_DTO)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); } @ParameterizedTest @MethodSource("invalidUsers") void createInvalidUser(UserCreateDto user) throws Exception { + Mockito.when(userFacade.create(ArgumentMatchers.isA(UserCreateDto.class))).thenReturn(USER_DTO); mockMvc.perform(post("/users") .content(asJsonString(user)) .contentType(MediaType.APPLICATION_JSON)) @@ -116,65 +147,47 @@ class UserControllerTest { @Test void findUser() throws Exception { - String response = mockMvc.perform(post("/users") - .content(asJsonString(USER_TO_CREATE)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - - String id = objectMapper.readValue(response, UserDto.class).getId(); - + long id = 1; + Mockito.when(userFacade.find(id)).thenReturn(USER_DTO); mockMvc.perform(get("/users/{id}", id)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(id)); + .andExpect(jsonPath("$.username").value(USER_DTO.getUsername())); } @Test void findAll() throws Exception { - mockMvc.perform(get("/users/all") - .param("page", "0")) - .andExpect(status().isOk()); + int page = 0; + Page<UserDto> pageUserDto = new PageImpl<>(List.of(USER_DTO)); + Mockito.when(userFacade.findAll(page)).thenReturn(pageUserDto); + + mockMvc.perform(get("/users") + .param("page", Integer.toString(page))) + .andExpect(status().isOk()) + .andExpect(content().string(objectMapper.writeValueAsString(pageUserDto))); } @Test void deleteUser() throws Exception { - String response = mockMvc.perform(post("/users") - .content(asJsonString(USER_TO_CREATE)) + mockMvc.perform(delete("/users/{id}", 1L) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - - String id = objectMapper.readValue(response, UserDto.class).getId(); - - mockMvc.perform(delete("/users/{id}", id) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); } @Test void update() throws Exception { - String response = mockMvc.perform(post("/users") - .content(asJsonString(USER_TO_CREATE)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - - String id = objectMapper.readValue(response, UserDto.class).getId(); + Long id = 1L; + Mockito.when(userFacade.update(id, USER_CREATE_DTO)).thenReturn(USER_DTO); - var updatedUsername = "novak"; - var userToUpdate = new UserCreateDto( - USER_TO_CREATE.getUsername(), USER_TO_CREATE.getPassword(), USER_TO_CREATE.getEmail(), - USER_TO_CREATE.getFirstName(), USER_TO_CREATE.getLastName(), USER_TO_CREATE.getAddress()); - userToUpdate.setUsername(updatedUsername); - - mockMvc.perform(put("/users/update/{id}", id) - .content(asJsonString(userToUpdate)) + mockMvc.perform(put("/users/{id}", id) + .content(asJsonString(USER_CREATE_DTO)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.username").value(updatedUsername)); + .andExpect(status().isOk()); } @Test void login() throws Exception { mockMvc.perform(post("/users/login") - .content(asJsonString(USER_TO_LOGIN)) + .content(asJsonString(USER_LOGIN_DTO)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } @@ -190,32 +203,49 @@ class UserControllerTest { @Test void logout() throws Exception { - String response = mockMvc.perform(post("/users") - .content(asJsonString(USER_TO_CREATE)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - - String id = objectMapper.readValue(response, UserDto.class).getId(); - - mockMvc.perform(post("/users/logout/{id}", id)) + mockMvc.perform(post("/users/{id}/logout", 1L)) .andExpect(status().isOk()); } @Test - void getFinished() throws Exception { - mockMvc.perform(get("/users/finished/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af")) - .andExpect(status().isOk()); + void getFinishedCourses() throws Exception { + Long id = 1L; + String name = "History Spanish"; + List<CourseDto> courses = List.of( + new CourseDto( name, 10, LanguageTypeDto.SPANISH, ProficiencyLevelDto.B2) + ); + Mockito.when(userFacade.getFinished(id)).thenReturn(courses); + mockMvc.perform(get("/users/{id}/finished-courses", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].name", equalTo(name))); } @Test - void getEnrolled() throws Exception { - mockMvc.perform(get("/users/enrolled/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af")) - .andExpect(status().isOk()); + void getEnrolledCourses() throws Exception { + Long id = 1L; + String name = "History Spanish"; + List<CourseDto> courses = List.of( + new CourseDto(name, 10, LanguageTypeDto.SPANISH, ProficiencyLevelDto.B2) + ); + Mockito.when(userFacade.getEnrolled(id)).thenReturn(courses); + mockMvc.perform(get("/users/{id}/courses", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].name", equalTo(name))); } @Test void addLanguage() throws Exception { - mockMvc.perform(put("/users/addLanguage/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af") + Long id = 1L; + var language = LanguageTypeDto.ENGLISH; + var proficiency = ProficiencyLevelDto.B2; + UserAddLanguageDto languageDto = new UserAddLanguageDto(language, proficiency); + UserDto userWithLanguages = USER_DTO; + userWithLanguages.setLanguageProficiency(Map.of(language, proficiency)); + + Mockito.when(userFacade.addLanguageProficiency(id, languageDto)).thenReturn(userWithLanguages); + mockMvc.perform(put("/users/{id}/languages", id) .content(asJsonString(new UserAddLanguageDto(LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2))) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserFacadeTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5fce17087b4217a9d232b0ffb5bf53615ab31b54 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserFacadeTest.java @@ -0,0 +1,159 @@ +package org.fuseri.modulelanguageschool.user; + +import org.fuseri.model.dto.course.CourseDto; +import org.fuseri.model.dto.course.LanguageTypeDto; +import org.fuseri.model.dto.course.ProficiencyLevelDto; +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserAddLanguageDto; +import org.fuseri.model.dto.user.UserCreateDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.CourseMapper; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.junit.jupiter.api.Test; +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.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SpringBootTest +@AutoConfigureMockMvc +final class UserFacadeTest { + + @Autowired + private UserFacade userFacade; + + @MockBean + private UserService userService; + + @MockBean + private UserMapper userMapper; + + @MockBean + private CourseMapper courseMapper; + + private static final LanguageTypeDto LANGUAGE_DTO = LanguageTypeDto.ENGLISH; + private static final ProficiencyLevelDto PROFICIENCY_DTO = ProficiencyLevelDto.B2; + private static final Map<LanguageTypeDto, ProficiencyLevelDto> LANGUAGE_PROFICIENCY_DTO = + Map.of(LANGUAGE_DTO, PROFICIENCY_DTO); + private final static List<CourseDto> COURSE_DTO_LIST = List.of(new CourseDto( "AJ1", 10, + LANGUAGE_DTO, PROFICIENCY_DTO)); + private static final List<Course> COURSE_LIST = List.of(new Course("AJ1", 10, + Language.valueOf(LANGUAGE_DTO.name()), ProficiencyLevel.valueOf(PROFICIENCY_DTO.name()))); + private static final Address ADDRESS = new Address( + "Czechia", "Brno", "Masarykova", "45", "90033"); + private static final AddressDto ADDRESS_DTO = new AddressDto( + "Czechia", "Brno", "Masarykova", "45", "90033"); + private final UserCreateDto USER_CREATE_DTO = new UserCreateDto( + "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af", + "xnovak@emample.com", "Peter", "Novak", ADDRESS_DTO, org.fuseri.model.dto.user.UserType.STUDENT, LANGUAGE_PROFICIENCY_DTO); + private final UserDto USER_DTO = new UserDto("xnovak", "xnovak@emample.com", "Peter", "Novak", ADDRESS_DTO, org.fuseri.model.dto.user.UserType.STUDENT); + private final User USER = new User( + "xnovak", UserType.STUDENT, "1234fak", "xnovak@emample.com", "Peter", "Novak", ADDRESS, Set.of(), Map.of()); + + @Test + void find() { + long id = 1L; + when(userService.find(id)).thenReturn(USER); + when(userMapper.toDto(USER)).thenReturn(USER_DTO); + + UserDto actualDto = userFacade.find(id); + assertNotNull(actualDto); + assertEquals(USER_DTO, actualDto); + } + + @Test + void create() { + when(userMapper.fromCreateDto(USER_CREATE_DTO)).thenReturn(USER); + when(userService.create(USER)).thenReturn(USER); + when(userMapper.toDto(USER)).thenReturn(USER_DTO); + + UserDto actualDto = userFacade.create(USER_CREATE_DTO); + assertEquals(USER_DTO, actualDto); + } + + @Test + void delete() { + long id = 1L; + userFacade.delete(id); + verify(userService).delete(id); + } + + @Test + void update() { + long id = 1L; + when(userMapper.fromCreateDto(USER_CREATE_DTO)).thenReturn(USER); + when(userService.update(id, USER)).thenReturn(USER); + when(userMapper.toDto(USER)).thenReturn(USER_DTO); + + UserDto actualDto = userFacade.update(id, USER_CREATE_DTO); + + assertEquals(USER_DTO, actualDto); + } + + @Test + void findAll() { + Pageable pageable = PageRequest.of(0, 10); + Page<User> userPage = new PageImpl<>(List.of(USER), pageable, 1); + Page<UserDto> expectedPageDto = new PageImpl<>(List.of(USER_DTO), pageable, 1); + + when(userService.findAll(pageable)).thenReturn(userPage); + when(userMapper.toDtoPage(userPage)).thenReturn(expectedPageDto); + + Page<UserDto> actualPageDto = userFacade.findAll(0); + + assertEquals(expectedPageDto, actualPageDto); + } + + @Test + void addLanguage() { + long id = 1L; + UserAddLanguageDto dto = new UserAddLanguageDto(LANGUAGE_DTO, PROFICIENCY_DTO); + when(userService.addLanguageProficiency( + id, Language.valueOf(LANGUAGE_DTO.name()), ProficiencyLevel.valueOf(PROFICIENCY_DTO.name()))) + .thenReturn(USER); + when(userMapper.toDto(USER)).thenReturn(USER_DTO); + + UserDto actualDto = userFacade.addLanguageProficiency(id, dto); + + assertNotNull(actualDto); + assertEquals(USER_DTO, actualDto); + } + + @Test + void getEnrolled() { + long id = 1L; + when(courseMapper.mapToList(COURSE_LIST)).thenReturn(COURSE_DTO_LIST); + when(userService.getEnrolled(id)).thenReturn(COURSE_LIST); + List<CourseDto> actualDtos = userFacade.getEnrolled(id); + + assertNotNull(actualDtos); + assertEquals(COURSE_DTO_LIST, actualDtos); + } + + @Test + void getFinished() { + long id = 1L; + when(courseMapper.mapToList(COURSE_LIST)).thenReturn(COURSE_DTO_LIST); + when(userService.getFinished(id)).thenReturn(COURSE_LIST); + List<CourseDto> actualDtos = userFacade.getFinished(id); + + assertNotNull(actualDtos); + assertEquals(COURSE_DTO_LIST, actualDtos); + } +} diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserMapperTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..88eaeabaf0c4b61af264536090004f021f26bc95 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserMapperTest.java @@ -0,0 +1,116 @@ +package org.fuseri.modulelanguageschool.user; + +import org.fuseri.model.dto.user.AddressDto; +import org.fuseri.model.dto.user.UserCreateDto; +import org.fuseri.model.dto.user.UserDto; +import org.fuseri.model.dto.user.UserType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class UserMapperTest { + + @Autowired + private UserMapper userMapper; + + private final UserDto userDto = new UserDto( + "xnovak", "xnovak@emample.com", "Peter", "Novak", + new AddressDto(), UserType.STUDENT); + private final UserCreateDto userCreateDto = new UserCreateDto( + "xnovak", "akfksobg", + "xnovak@emample.com", "Peter", "Novak", new AddressDto(), UserType.STUDENT, Map.of()); + + private final User userFromCreateDto = new User( + "xnovak", org.fuseri.modulelanguageschool.user.UserType.STUDENT, "akfksobg", + "xnovak@emample.com", "Peter", "Novak", new Address(), null, new HashMap<>()); + + private final User userFromDto = new User( + "xnovak", org.fuseri.modulelanguageschool.user.UserType.STUDENT, null, + "xnovak@emample.com", "Peter", "Novak", new Address(), null, null); + + @Test + void nullFromCreateDto() { + User mappedUser = userMapper.fromCreateDto(null); + assertNull(mappedUser); + } + + @Test + void fromCreateDto() { + User mappedUser = userMapper.fromCreateDto(userCreateDto); + assertEquals(userFromCreateDto, mappedUser); + } + + @Test + void nullFromDto() { + User mappedUser = userMapper.fromDto(null); + assertNull(mappedUser); + } + + @Test + void fromDto() { + User mappedUser = userMapper.fromDto(userDto); + assertEquals(userFromDto, mappedUser); + } + + @Test + void nullToDto() { + var createdDto = userMapper.toDto(null); + assertNull(createdDto); + } + + @Test + void toDto() { + var mappedDto = userMapper.toDto(userFromDto); + assertEquals(userDto, mappedDto); + } + + @Test + void nullToDtoList() { + var mappedDtos = userMapper.toDtoList(null); + assertNull(mappedDtos); + } + + @Test + void toEmptyDtoList() { + var mappedDtos = userMapper.toDtoList(Collections.emptyList()); + assertTrue(mappedDtos.isEmpty()); + } + + @Test + void toDtoList() { + var mappedDtos = userMapper.toDtoList(List.of(userFromDto)); + assertEquals(List.of(userDto), mappedDtos); + } + + @Test + void toEmptyDtoPage() { + Page<UserDto> pageDto = userMapper.toDtoPage(Page.empty()); + assertEquals(1, pageDto.getTotalPages()); + assertTrue(pageDto.getContent().isEmpty()); + } + + @Test + void toDtoPage() { + List<User> users = List.of(userFromDto); + Page<User> page = new PageImpl<>(users, PageRequest.of(0, 10), users.size()); + Page<UserDto> pageDto = userMapper.toDtoPage(page); + + assertEquals(page.getTotalPages(), pageDto.getTotalPages()); + assertEquals(page.getNumber(), pageDto.getNumber()); + assertEquals(page.getNumberOfElements(), pageDto.getNumberOfElements()); + assertEquals(page.getSize(), pageDto.getSize()); + assertEquals(page.getTotalElements(), pageDto.getTotalElements()); + assertEquals(List.of(userDto), pageDto.getContent()); + } +} \ No newline at end of file diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserRepositoryTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserRepositoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3538aff2c95173f76d0cb2e67c78f86a39b601e5 --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserRepositoryTest.java @@ -0,0 +1,57 @@ +package org.fuseri.modulelanguageschool.user; + +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DataJpaTest +class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private TestEntityManager entityManager; + + private final Course course = new Course("AJ1", 10, Language.ENGLISH, ProficiencyLevel.B2); + private final Set<Course> COURSES = Set.of(course); + private final User user = new User( + "xnovak", UserType.STUDENT, + "1234fak", "xnovak@emample.com", "Peter", "Novak", + new Address(), COURSES, Map.of()); + + @Test + void getEnrolled() { + entityManager.persist(course); + entityManager.persist(user); + entityManager.flush(); + + List<Course> foundCourses = userRepository.getEnrolled(user.getId()); + assertNotNull(foundCourses); + assertEquals(new ArrayList<>(COURSES), foundCourses); + } + + @Test + void getFinished() { + course.setFinished(true); + entityManager.persist(course); + entityManager.persist(user); + entityManager.flush(); + + List<Course> foundCourses = userRepository.getFinished(user.getId()); + assertNotNull(foundCourses); + assertEquals(new ArrayList<>(COURSES), foundCourses); + } +} \ No newline at end of file diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserServiceTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..062bc734f1a3e8778a951432eaf93f9b6b76e3fc --- /dev/null +++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserServiceTest.java @@ -0,0 +1,76 @@ +package org.fuseri.modulelanguageschool.user; + +import org.fuseri.modulelanguageschool.course.Course; +import org.fuseri.modulelanguageschool.course.Language; +import org.fuseri.modulelanguageschool.course.ProficiencyLevel; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SpringBootTest +class UserServiceTest { + + @Autowired + private UserService userService; + + @MockBean + private UserRepository userRepository; + + private static final User USER = new User( + "xnovak", UserType.STUDENT, "1234fak", "xnovak@emample.com", "Peter", "Novak", new Address(), Set.of(), new HashMap<>()); + private static final Course COURSE = new Course("AJ1", 10, Language.ENGLISH, ProficiencyLevel.B2); + private static final List<Course> COURSE_LIST = List.of(COURSE); + private static final User USER_WITH_ENROLLED_COURSE = new User( + "xnovak", UserType.STUDENT, "1234fak", "xnovak@emample.com", "Peter", "Novak", new Address(), Set.of(COURSE), Map.of()); + + @Test + void find() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(USER)); + User actualUser = userService.find(anyLong()); + assertEquals(USER, actualUser); + } + + @Test + void getEnrolled() { + Long id = 1L; + when(userRepository.getEnrolled(anyLong())).thenReturn(COURSE_LIST); + List<Course> actualCourses = userService.getEnrolled(id); + assertEquals(COURSE_LIST, actualCourses); + } + + @Test + void getFinished() { + long id = 1L; + when(userRepository.getFinished(anyLong())).thenReturn(COURSE_LIST); + List<Course> actualCourses = userService.getFinished(id); + assertEquals(COURSE_LIST, actualCourses); + } + + @Test + void addLanguageProficiency() { + long id = 1L; + Language language = Language.ENGLISH; + ProficiencyLevel proficiencyLevel = ProficiencyLevel.B2; + when(userRepository.findById(id)).thenReturn(Optional.of(USER)); + when(userRepository.save(any(User.class))).thenReturn(USER_WITH_ENROLLED_COURSE); + + User updatedUser = userService.addLanguageProficiency(id, language, proficiencyLevel); + assertEquals(USER_WITH_ENROLLED_COURSE, updatedUser); + verify(userRepository).findById(id); + verify(userRepository).save(any(User.class)); + } +} \ No newline at end of file diff --git a/application/module-language-school/src/test/resources/application-test.properties b/application/module-language-school/src/test/resources/application-test.properties new file mode 100644 index 0000000000000000000000000000000000000000..896213ef130ffc73d291c5d76336ccf6aaef4f9f --- /dev/null +++ b/application/module-language-school/src/test/resources/application-test.properties @@ -0,0 +1,14 @@ +# For description of each field check: https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# INIT=CREATE SCHEMA IF NOT EXISTS PA165;SET SCHEMA PA165 +spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=SedaQ-app-test +spring.datasource.password= +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.properties.hibernate.generate_statistics=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=false +spring.h2.console.enabled=true +spring.cache.type=NONE +appconfig.enablecache=false \ No newline at end of file diff --git a/application/module-language-school/src/test/resources/logback.xml b/application/module-language-school/src/test/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..e449f9cd302a0db94a3f6d8469468a7b277ca638 --- /dev/null +++ b/application/module-language-school/src/test/resources/logback.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d %5p %40.40c:%4L - %m%n</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="console" /> + </root> +<!-- TODO remove solution--> + <logger name="org.hibernate.SQL" level="DEBUG"/> + +</configuration> \ No newline at end of file diff --git a/application/module-mail/Dockerfile b/application/module-mail/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2e3cde273701942cb4330be21845cd681a7bb084 --- /dev/null +++ b/application/module-mail/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.io/library/eclipse-temurin:17-jre-focal +COPY ./target/module-mail-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/application/pipeline.yml b/application/pipeline.yml index 2b29b6dd76dd775858d9b52bc2f6c7d185c40b29..58a8f7ffe73ca37ef5263e501cb0ddd83b9c0cd2 100644 --- a/application/pipeline.yml +++ b/application/pipeline.yml @@ -1,7 +1,9 @@ # This file specifies GitLab CI/CD pipeline, see https://docs.gitlab.com/ee/ci/ stages: + - build - test + - deploy variables: # variable read by Maven for JVM options, see https://maven.apache.org/configure.html#maven_opts-environment-variable @@ -9,19 +11,64 @@ variables: # our own variable for repeated options MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version " -test: +# name of Docker image in which the script commands are executed +image: maven:latest + +cache: + paths: + - .m2/repository + +build: + stage: build + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS clean install -DskipTests' + +test-certificate: stage: test tags: - shared-fi - # name of Docker image in which the script commands are executed image: maven:latest - # script is a list of linux shell commands script: - # run maven build - 'cd "$CI_PROJECT_DIR/application"' - - 'mvn $MAVEN_CLI_OPTS clean install' - cache: - # caches maven repo between runs - paths: - - .m2/repository + - 'mvn $MAVEN_CLI_OPTS -am -pl module-certificate test' +test-exercise: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-exercise test' + +test-language-school: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-language-school test' + +test-mail: + stage: test + tags: + - shared-fi + image: maven:latest + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS -am -pl module-mail test' + +deploy: + when: manual + stage: deploy + script: + - 'cd "$CI_PROJECT_DIR/application"' + - 'mvn $MAVEN_CLI_OPTS install -DskipTests' + - 'docker-compose build --parallel' + - 'docker login -u $DOCKER_PRIVATE_LOGIN -p $DOCKER_PRIVATE_PASSWORD' + - 'docker-compose push' diff --git a/application/podman-compose.yml b/application/podman-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..41a6a0d3edb0fa3ffa8d88abb80419ffb7e96025 --- /dev/null +++ b/application/podman-compose.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + certificate: + build: ./module-certificate + container_name: certificate + image: xpokorn8/sprachschulsystem:certificate + ports: + - "5001:5001" + + exercise: + build: ./module-exercise + container_name: exercise + image: xpokorn8/sprachschulsystem:exercise + ports: + - "5002:5002" + + language-school: + build: ./module-language-school + container_name: language-school + image: xpokorn8/sprachschulsystem:language-school + ports: + - "5000:5000" + + mail: + build: ./module-mail + container_name: mail + image: xpokorn8/sprachschulsystem:mail + ports: + - "5003:5003" \ No newline at end of file