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