diff --git a/dev.env b/dev.env new file mode 100644 index 0000000..8ff9534 --- /dev/null +++ b/dev.env @@ -0,0 +1,4 @@ +DB_URL=localhost +DB_NAME=postgres_db +DB_USER=postgres +DB_PASSWORD= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..74a47d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.5' + +services: + postgres-automaton: + container_name: postgres_demo_application + image: postgres:latest + ports: + - "5432:5432" + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + PGDATA: /data/postgres + volumes: + - ./postgres-db:/data/postgres \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2c23e76..60941b5 100644 --- a/pom.xml +++ b/pom.xml @@ -24,30 +24,38 @@ 11 - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + @@ -58,5 +66,4 @@ - diff --git a/src/main/java/com/example/postgresdemo/controller/AnswerController.java b/src/main/java/com/example/postgresdemo/controller/AnswerController.java index 7cfaa47..5af8f7b 100644 --- a/src/main/java/com/example/postgresdemo/controller/AnswerController.java +++ b/src/main/java/com/example/postgresdemo/controller/AnswerController.java @@ -1,66 +1,66 @@ -package com.example.postgresdemo.controller; - -import com.example.postgresdemo.exception.ResourceNotFoundException; -import com.example.postgresdemo.model.Answer; -import com.example.postgresdemo.repository.AnswerRepository; -import com.example.postgresdemo.repository.QuestionRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; -import java.util.List; - -@RestController -public class AnswerController { - - @Autowired - private AnswerRepository answerRepository; - - @Autowired - private QuestionRepository questionRepository; - - @GetMapping("/questions/{questionId}/answers") - public List getAnswersByQuestionId(@PathVariable Long questionId) { - return answerRepository.findByQuestionId(questionId); - } - - @PostMapping("/questions/{questionId}/answers") - public Answer addAnswer(@PathVariable Long questionId, - @Valid @RequestBody Answer answer) { - return questionRepository.findById(questionId) - .map(question -> { - answer.setQuestion(question); - return answerRepository.save(answer); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } - - @PutMapping("/questions/{questionId}/answers/{answerId}") - public Answer updateAnswer(@PathVariable Long questionId, - @PathVariable Long answerId, - @Valid @RequestBody Answer answerRequest) { - if(!questionRepository.existsById(questionId)) { - throw new ResourceNotFoundException("Question not found with id " + questionId); - } - - return answerRepository.findById(answerId) - .map(answer -> { - answer.setText(answerRequest.getText()); - return answerRepository.save(answer); - }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); - } - - @DeleteMapping("/questions/{questionId}/answers/{answerId}") - public ResponseEntity deleteAnswer(@PathVariable Long questionId, - @PathVariable Long answerId) { - if(!questionRepository.existsById(questionId)) { - throw new ResourceNotFoundException("Question not found with id " + questionId); - } - - return answerRepository.findById(answerId) - .map(answer -> { - answerRepository.delete(answer); - return ResponseEntity.ok().build(); - }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); - - } -} +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Answer; +import com.example.postgresdemo.repository.AnswerRepository; +import com.example.postgresdemo.repository.QuestionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; +import java.util.List; + +@RestController +public class AnswerController { + + @Autowired + private AnswerRepository answerRepository; + + @Autowired + private QuestionRepository questionRepository; + + @GetMapping("/questions/{questionId}/answers") + public List getAnswersByQuestionId(@PathVariable Long questionId) { + return answerRepository.findByQuestionId(questionId); + } + + @PostMapping("/questions/{questionId}/answers") + public Answer addAnswer(@PathVariable Long questionId, + @Valid @RequestBody Answer answer) { + return questionRepository.findById(questionId) + .map(question -> { + answer.setQuestion(question); + return answerRepository.save(answer); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + } + + @PutMapping("/questions/{questionId}/answers/{answerId}") + public Answer updateAnswer(@PathVariable Long questionId, + @PathVariable Long answerId, + @Valid @RequestBody Answer answerRequest) { + if(!questionRepository.existsById(questionId)) { + throw new ResourceNotFoundException("Question not found with id " + questionId); + } + + return answerRepository.findById(answerId) + .map(answer -> { + answer.setText(answerRequest.getText()); + return answerRepository.save(answer); + }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); + } + + @DeleteMapping("/questions/{questionId}/answers/{answerId}") + public ResponseEntity deleteAnswer(@PathVariable Long questionId, + @PathVariable Long answerId) { + if(!questionRepository.existsById(questionId)) { + throw new ResourceNotFoundException("Question not found with id " + questionId); + } + + return answerRepository.findById(answerId) + .map(answer -> { + answerRepository.delete(answer); + return ResponseEntity.ok().build(); + }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); + + } +} diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java index c231819..90b0884 100644 --- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java +++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java @@ -1,50 +1,39 @@ -package com.example.postgresdemo.controller; - -import com.example.postgresdemo.exception.ResourceNotFoundException; -import com.example.postgresdemo.model.Question; -import com.example.postgresdemo.repository.QuestionRepository; -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 javax.validation.Valid; - -@RestController -public class QuestionController { - - @Autowired - private QuestionRepository questionRepository; - - @GetMapping("/questions") - public Page getQuestions(Pageable pageable) { - return questionRepository.findAll(pageable); - } - - - @PostMapping("/questions") - public Question createQuestion(@Valid @RequestBody Question question) { - return questionRepository.save(question); - } - - @PutMapping("/questions/{questionId}") - public Question updateQuestion(@PathVariable Long questionId, - @Valid @RequestBody Question questionRequest) { - return questionRepository.findById(questionId) - .map(question -> { - question.setTitle(questionRequest.getTitle()); - question.setDescription(questionRequest.getDescription()); - return questionRepository.save(question); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } - - - @DeleteMapping("/questions/{questionId}") - public ResponseEntity deleteQuestion(@PathVariable Long questionId) { - return questionRepository.findById(questionId) - .map(question -> { - questionRepository.delete(question); - return ResponseEntity.ok().build(); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } -} +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.service.QuestionService; +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 javax.validation.Valid; + +@RestController +public class QuestionController { + @Autowired + private QuestionService questionService; + + @GetMapping("/questions") + public Page getQuestions(Pageable pageable) { + return questionService.findAll(pageable); + } + + @PostMapping("/questions") + public QuestionResponseDTO createQuestion(@Valid @RequestBody QuestionRequestDTO question) { + return questionService.create(question); + } + + @PutMapping("/questions/{questionId}") + public QuestionResponseDTO updateQuestion(@PathVariable Long questionId, + @Valid @RequestBody QuestionRequestDTO questionRequest) { + return questionService.update(questionId, questionRequest); + } + + @DeleteMapping("/questions/{questionId}") + public void deleteQuestion(@PathVariable Long questionId) { + questionService.delete(questionId); + } +} diff --git a/src/main/java/com/example/postgresdemo/model/Answer.java b/src/main/java/com/example/postgresdemo/model/Answer.java index b5e48d0..94f8c8b 100644 --- a/src/main/java/com/example/postgresdemo/model/Answer.java +++ b/src/main/java/com/example/postgresdemo/model/Answer.java @@ -1,53 +1,53 @@ -package com.example.postgresdemo.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -import javax.persistence.*; - -@Entity -@Table(name = "answers") -public class Answer extends AuditModel { - @Id - @GeneratedValue(generator = "answer_generator") - @SequenceGenerator( - name = "answer_generator", - sequenceName = "answer_sequence", - initialValue = 1000 - ) - private Long id; - - @Column(columnDefinition = "text") - private String text; - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "question_id", nullable = false) - @OnDelete(action = OnDeleteAction.CASCADE) - @JsonIgnore - private Question question; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public Question getQuestion() { - return question; - } - - public void setQuestion(Question question) { - this.question = question; - } -} +package com.example.postgresdemo.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Table(name = "answers") +public class Answer extends AuditModel { + @Id + @GeneratedValue(generator = "answer_generator") + @SequenceGenerator( + name = "answer_generator", + sequenceName = "answer_sequence", + initialValue = 1000 + ) + private Long id; + + @Column(columnDefinition = "text") + private String text; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "question_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + private Question question; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/AuditModel.java b/src/main/java/com/example/postgresdemo/model/AuditModel.java index a61a3b8..06e17cb 100644 --- a/src/main/java/com/example/postgresdemo/model/AuditModel.java +++ b/src/main/java/com/example/postgresdemo/model/AuditModel.java @@ -1,43 +1,43 @@ -package com.example.postgresdemo.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.*; -import java.io.Serializable; -import java.util.Date; - -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -@JsonIgnoreProperties( - value = {"createdAt", "updatedAt"}, - allowGetters = true -) -public abstract class AuditModel implements Serializable { - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_at", nullable = false, updatable = false) - @CreatedDate - private Date createdAt; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "updated_at", nullable = false) - @LastModifiedDate - private Date updatedAt; - - public Date getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Date createdAt) { - this.createdAt = createdAt; - } - - public Date getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Date updatedAt) { - this.updatedAt = updatedAt; - } +package com.example.postgresdemo.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Date; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@JsonIgnoreProperties( + value = {"createdAt", "updatedAt"}, + allowGetters = true +) +public abstract class AuditModel implements Serializable { + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + @CreatedDate + private Date createdAt; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at", nullable = false) + @LastModifiedDate + private Date updatedAt; + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } } \ No newline at end of file diff --git a/src/main/java/com/example/postgresdemo/model/Question.java b/src/main/java/com/example/postgresdemo/model/Question.java index d16a459..85193a8 100644 --- a/src/main/java/com/example/postgresdemo/model/Question.java +++ b/src/main/java/com/example/postgresdemo/model/Question.java @@ -1,49 +1,67 @@ -package com.example.postgresdemo.model; - -import javax.persistence.*; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; - -@Entity -@Table(name = "questions") -public class Question extends AuditModel { - @Id - @GeneratedValue(generator = "question_generator") - @SequenceGenerator( - name = "question_generator", - sequenceName = "question_sequence", - initialValue = 1000 - ) - private Long id; - - @NotBlank - @Size(min = 3, max = 100) - private String title; - - @Column(columnDefinition = "text") - private String description; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} +package com.example.postgresdemo.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "questions") +public class Question extends AuditModel { + @Id + @GeneratedValue(generator = "question_generator") + @SequenceGenerator( + name = "question_generator", + sequenceName = "question_sequence", + initialValue = 1000 + ) + private Long id; + + @NotBlank + @Size(min = 3, max = 100) + private String title; + + @Column(columnDefinition = "text") + private String description; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + private User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java new file mode 100644 index 0000000..80a213a --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java @@ -0,0 +1,40 @@ +package com.example.postgresdemo.model; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public class QuestionRequestDTO { + @NotBlank + @Size(min = 3, max = 100) + private String title; + + private String description; + + @NotNull + private Long authorId; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Long getAuthorId() { + return authorId; + } + + public void setAuthorId(Long authorId) { + this.authorId = authorId; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java new file mode 100644 index 0000000..924ecbe --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java @@ -0,0 +1,22 @@ +package com.example.postgresdemo.model; + +public class QuestionResponseDTO { + private Long id; + private String body; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/User.java b/src/main/java/com/example/postgresdemo/model/User.java new file mode 100644 index 0000000..d2cb3b9 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/User.java @@ -0,0 +1,57 @@ +package com.example.postgresdemo.model; + +import javax.persistence.*; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue + private Long id; + @Size(min = 1, max = 50) + private String firstname; + @Size(min = 1, max = 50) + private String lastname; + private String password; + + public User() {} + + public User(Long id, String firstname, String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java index 290373d..c5d4d41 100644 --- a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java +++ b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java @@ -1,9 +1,9 @@ -package com.example.postgresdemo.repository; - -import com.example.postgresdemo.model.Question; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QuestionRepository extends JpaRepository { -} +package com.example.postgresdemo.repository; + +import com.example.postgresdemo.model.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuestionRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/postgresdemo/repository/UserRepository.java b/src/main/java/com/example/postgresdemo/repository/UserRepository.java new file mode 100644 index 0000000..c04c909 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.postgresdemo.repository; + +import com.example.postgresdemo.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java new file mode 100644 index 0000000..2352cdc --- /dev/null +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -0,0 +1,72 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.model.User; +import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +public class QuestionService { + private final QuestionRepository questionRepository; + private final UserRepository userRepository; + + + public QuestionService(QuestionRepository questionRepository, UserRepository userRepository) { + this.questionRepository = questionRepository; + this.userRepository = userRepository; + } + + public Page findAll(Pageable pageable) { + return questionRepository.findAll(pageable) + .map(this::toQuestionResponseDTO); + } + + public QuestionResponseDTO create(QuestionRequestDTO questionRequest) { + User user = userRepository.findById(questionRequest.getAuthorId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + questionRequest.getAuthorId())); + Question question = toQuestion(questionRequest, user); + return toQuestionResponseDTO(questionRepository.save(question)); + } + + public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRequest) { + User user = userRepository.findById(questionRequest.getAuthorId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + questionRequest.getAuthorId())); + Question question = toQuestion(questionRequest, user); + return questionRepository.findById(questionId) + .map(foundQuestion -> { + foundQuestion.setTitle(question.getTitle()); + foundQuestion.setDescription(question.getDescription()); + foundQuestion.setUser(user); + return toQuestionResponseDTO(questionRepository.save(foundQuestion)); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + } + + public void delete(Long questionId) { + Question question = questionRepository.findById(questionId) + .orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + questionRepository.delete(question); + } + + private QuestionResponseDTO toQuestionResponseDTO(Question question) { + QuestionResponseDTO questionResponse = new QuestionResponseDTO(); + questionResponse.setId(question.getId()); + questionResponse.setBody(String.join("\n", question.getTitle(), question.getDescription())); + + + return questionResponse; + } + + private Question toQuestion(QuestionRequestDTO questionRequestDTO, User user) { + Question question = new Question(); + question.setTitle(questionRequestDTO.getTitle()); + question.setDescription(questionRequestDTO.getDescription()); + question.setUser(user); + return question; + } +} diff --git a/src/main/java/com/example/postgresdemo/service/UserService.java b/src/main/java/com/example/postgresdemo/service/UserService.java new file mode 100644 index 0000000..a5e8819 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/service/UserService.java @@ -0,0 +1,41 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.User; +import com.example.postgresdemo.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public List findAll() { + return userRepository.findAll(); + } + + public User create(User user) { + return userRepository.save(user); + } + + public User update(Long userId, User userDetails) { + return userRepository.findById(userId) + .map(user -> { + user.setFirstname(userDetails.getFirstname()); + user.setLastname(userDetails.getLastname()); + return userRepository.save(user); + }).orElseThrow(() -> new ResourceNotFoundException("User not found with id " + userId)); + } + + + public void delete(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + userId)); + userRepository.delete(user); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 35b376a..969a5b6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,10 @@ ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) -spring.datasource.url=jdbc:postgresql://localhost:5432/postgres_demo -spring.datasource.username= postgres -spring.datasource.password= +spring.datasource.url=jdbc:postgresql://${DB_URL}:5432/${DB_NAME} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} # The SQL dialect makes Hibernate generate better SQL for the chosen database -spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # Hibernate ddl auto (create, create-drop, validate, update) -spring.jpa.hibernate.ddl-auto = update +spring.jpa.hibernate.ddl-auto=update diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java new file mode 100644 index 0000000..61aeec9 --- /dev/null +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -0,0 +1,215 @@ +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.User; +import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.nio.CharBuffer; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class QuestionControllerTest { + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private MockMvc mockMvc; + + private Long userId; + + @BeforeEach + void setup() { + User user = new User(); + user.setFirstname("John"); + user.setLastname("Doe"); + userRepository.save(user); + userId = user.getId(); + } + + @AfterEach + void deleteAll() { + questionRepository.deleteAll(); + userRepository.deleteAll(); + } + + @Test + void testGetQuestionsWithAmountLessThanPageSize() throws Exception { + int assertionNumber = 10; + int pageSize = 20; + + fillQuestions(assertionNumber); + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(1))); + } + + @Test + void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { + int assertionNumber = 30; + int pageSize = 20; + int totalPages = (int) Math.ceil(assertionNumber / (double) pageSize); + + fillQuestions(assertionNumber); + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(pageSize))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(totalPages))); + } + + @Test + void testCreateCorrectQuestion() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"description\": \"Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nDescription 1"))); + } + + @Test + void testCreateQuestionWithoutTitle() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithTitleLessThanThreeChars() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Te\",\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithTitleMoreThanHundredChars() throws Exception { + int numberOfChars = 101; + String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); + + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"" + title + "\",\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithoutDescription() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull"))); + } + + @Test + void testCreateQuestionWithoutAuthor() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"description\": \"Description 1\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testUpdateQuestion() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Edited Question 1\nEdited Description 1"))); + } + + @Test + void testUpdateQuestionWithNonExistingId() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + (questionId + 1)) + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testDeleteQuestion() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.delete("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + private void fillQuestions(Integer number) { + for (int i = 0; i < number; i++) { + Question question = new Question(); + question.setTitle("Question " + i); + question.setDescription("Description " + i); + question.setUser(userRepository.findById(userId).orElseThrow()); + questionRepository.save(question); + } + } +} diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java new file mode 100644 index 0000000..00c0b77 --- /dev/null +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -0,0 +1,204 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.model.User; +import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.*; +import org.springframework.data.domain.Page; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; + +@ExtendWith(MockitoExtension.class) +class QuestionServiceTest { + + @Mock + QuestionRepository questionRepository; + + @Mock + UserRepository userRepository; + + @InjectMocks + QuestionService questionService; + + @Captor + ArgumentCaptor questionCaptor; + + @Test + void testFindAll() { + Question question1 = new Question(); + question1.setId(1L); + question1.setTitle("Title1"); + question1.setDescription("Description1"); + Question question2 = new Question(); + question2.setId(2L); + question2.setTitle("Title2"); + question2.setDescription("Description2"); + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(Arrays.asList(question1, question2)); + Mockito.when(questionRepository.findAll(pageable)).thenReturn(page); + + Page result = questionService.findAll(pageable); + + Mockito.verify(questionRepository).findAll(pageable); + Assertions.assertEquals(2, result.getTotalElements()); + Assertions.assertEquals(1L, result.getContent().get(0).getId()); + Assertions.assertEquals("Title1\nDescription1", result.getContent().get(0).getBody()); + Assertions.assertEquals(2L, result.getContent().get(1).getId()); + Assertions.assertEquals("Title2\nDescription2", result.getContent().get(1).getBody()); + } + + @ParameterizedTest + @MethodSource("provideDescriptions") + void testCreateWithDescriptionVariations(String description, String expectedBody) { + Long authorId = 1L; + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription(description); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); + + Question question = new Question(); + question.setId(1L); + question.setTitle("Title"); + question.setDescription(description); + question.setUser(user); + + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); + Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(question); + + QuestionResponseDTO result = questionService.create(request); + + Mockito.verify(userRepository).findById(authorId); + Mockito.verify(questionRepository).save(questionCaptor.capture()); + + Question capturedQuestion = questionCaptor.getValue(); + + Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); + Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); + Assertions.assertEquals(user, capturedQuestion.getUser()); + Assertions.assertEquals(1L, result.getId()); + Assertions.assertEquals(expectedBody, result.getBody()); + } + + @Test + void testUpdateExistingQuestion() { + Long questionId = 123L; + Long authorId = 1L; + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription("Description"); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); + + Question questionToUpdate = new Question(); + questionToUpdate.setId(questionId); + questionToUpdate.setTitle("Old Title"); + questionToUpdate.setDescription("Old Description"); + questionToUpdate.setUser(user); + + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToUpdate)); + Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(questionToUpdate); + + QuestionResponseDTO result = questionService.update(questionId, request); + + Mockito.verify(userRepository).findById(authorId); + Mockito.verify(questionRepository).findById(questionId); + Mockito.verify(questionRepository).save(questionCaptor.capture()); + + Question capturedQuestion = questionCaptor.getValue(); + + Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); + Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); + Assertions.assertEquals(user, capturedQuestion.getUser()); + Assertions.assertEquals(questionId, result.getId()); + } + + @Test + void testUpdateNotExistingQuestion() { + Long questionId = 123L; + Long authorId = 1L; + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription("Description"); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); + + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + ResourceNotFoundException resourceNotFoundException = Assertions.assertThrows(ResourceNotFoundException.class, () -> { + questionService.update(questionId, request); + }); + + Assertions.assertEquals("Question not found with id " + questionId, resourceNotFoundException.getMessage()); + Mockito.verify(userRepository).findById(authorId); + Mockito.verify(questionRepository).findById(questionId); + Mockito.verifyNoMoreInteractions(questionRepository); + } + + @Test + void deleteExistingQuestion() { + Long questionId = 123L; + Question questionToDelete = new Question(); + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToDelete)); + + questionService.delete(questionId); + + Mockito.verify(questionRepository).findById(questionId); + Mockito.verify(questionRepository).delete(questionToDelete); + Mockito.verifyNoMoreInteractions(questionRepository); + } + + @Test + void deleteNonexistentQuestion() { + Long questionId = 123L; + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + ResourceNotFoundException resourceNotFoundException = Assertions.assertThrows(ResourceNotFoundException.class, () -> { + questionService.delete(questionId); + }); + Assertions.assertEquals("Question not found with id " + questionId, resourceNotFoundException.getMessage()); + Mockito.verify(questionRepository).findById(questionId); + + Mockito.verifyNoMoreInteractions(questionRepository); + } + + private static Stream provideDescriptions() { + return Stream.of( + Arguments.of(null, "Title\nnull"), + Arguments.of("", "Title\n"), + Arguments.of("Some description", "Title\nSome description") + ); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..6d94c03 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,14 @@ +spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL; +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +# We add the MySQL Dialect so that it understands and generates the query based on MySQL +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +spring.h2.console.enabled=true +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +#spring.jpa.properties.hibernate.show_sql=true + + +spring.sql.init.mode=always