From 8498c4e54440ce72eb7a9e2a908d28cdc6c58177 Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Fri, 25 Aug 2023 20:07:23 +0100
Subject: [PATCH 01/16] Get all cakes endpoint happy path
---
.gitignore | 13 +-
README.txt | 12 ++
pom.xml | 161 ++++++++++--------
.../java/com.waracle.cakemgr/CakeEntity.java | 52 ------
.../java/com.waracle.cakemgr/CakeServlet.java | 106 ------------
.../com.waracle.cakemgr/HibernateUtil.java | 36 ----
.../cakemanager/CakeManagerApplication.java | 13 ++
.../controller/CakeController.java | 21 +++
.../philldenness/cakemanager/dto/CakeDTO.java | 8 +
.../cakemanager/entity/CakeEntity.java | 23 +++
.../cakemanager/mapper/CakeMapper.java | 12 ++
.../repository/CakeRepository.java | 9 +
.../cakemanager/service/CakeService.java | 20 +++
src/main/resources/application.properties | 5 +
.../resources/db/migration/V1__Initial.sql | 8 +
src/main/resources/hibernate.cfg.xml | 17 --
src/main/webapp/WEB-INF/web.xml | 8 -
src/main/webapp/index.jsp | 5 -
.../controller/CakeControllerTest.java | 34 ++++
.../integrationTest/GetCakeIT.java | 45 +++++
.../cakemanager/mapper/CakeMapperTest.java | 31 ++++
.../cakemanager/service/CakeServiceTest.java | 47 +++++
src/test/resources/json/cakes-list.json | 102 +++++++++++
23 files changed, 489 insertions(+), 299 deletions(-)
delete mode 100644 src/main/java/com.waracle.cakemgr/CakeEntity.java
delete mode 100644 src/main/java/com.waracle.cakemgr/CakeServlet.java
delete mode 100644 src/main/java/com.waracle.cakemgr/HibernateUtil.java
create mode 100644 src/main/java/com/philldenness/cakemanager/CakeManagerApplication.java
create mode 100644 src/main/java/com/philldenness/cakemanager/controller/CakeController.java
create mode 100644 src/main/java/com/philldenness/cakemanager/dto/CakeDTO.java
create mode 100644 src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
create mode 100644 src/main/java/com/philldenness/cakemanager/mapper/CakeMapper.java
create mode 100644 src/main/java/com/philldenness/cakemanager/repository/CakeRepository.java
create mode 100644 src/main/java/com/philldenness/cakemanager/service/CakeService.java
create mode 100644 src/main/resources/application.properties
create mode 100644 src/main/resources/db/migration/V1__Initial.sql
delete mode 100644 src/main/resources/hibernate.cfg.xml
delete mode 100644 src/main/webapp/WEB-INF/web.xml
delete mode 100644 src/main/webapp/index.jsp
create mode 100644 src/test/java/com/philldenness/cakemanager/controller/CakeControllerTest.java
create mode 100644 src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
create mode 100644 src/test/java/com/philldenness/cakemanager/mapper/CakeMapperTest.java
create mode 100644 src/test/java/com/philldenness/cakemanager/service/CakeServiceTest.java
create mode 100644 src/test/resources/json/cakes-list.json
diff --git a/.gitignore b/.gitignore
index d81f12ed..f0de6717 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,11 @@
-/target
-/.idea
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
diff --git a/README.txt b/README.txt
index a0901c92..593bc1eb 100644
--- a/README.txt
+++ b/README.txt
@@ -55,3 +55,15 @@ Please provide your version of this project as a git repository (e.g. Github, Bi
A fork of this repo, or a Pull Request would be suitable.
Good luck!
+
+
+Notes from Phill
+==========
+
+Requires Java 20 to run.
+If running in intelliJ it requires lombok annotations to be enabled.
+I used TDD and 'The Double Loop' cycle which is described brilliantly here: https://jmauerhan.wordpress.com/talks/double-loop-tdd-bdd-done-right/
+Flyway migration scripts run automatically on startup
+
+Next steps:
+Add pagination and sorting to getAll endpoint
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c8cbf9d5..01bac950 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,77 +1,92 @@
- 4.0.0
- com.waracle
- cake-manager
- war
- 1.0-SNAPSHOT
- cake-manager Maven Webapp
- http://maven.apache.org
-
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.3
+
+
+ com.philldenness
+ cakemanager
+ 0.0.1-SNAPSHOT
+ cakemanager
+ Cake Manager Micro Service (fictitious)
+
+ 20
+ 2022.0.4
+ 9.21.2
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.flywaydb
+ flyway-core
+ ${flywaydb-core.version}
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-starter-contract-verifier
+ test
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
-
-
- javax.servlet
- javax.servlet-api
- 3.1.0
-
-
-
-
- com.fasterxml.jackson.core
- jackson-core
- 2.8.0
-
-
-
-
- org.hibernate
- hibernate-entitymanager
- 4.3.6.Final
-
-
-
-
- org.hsqldb
- hsqldb
- 2.3.4
-
-
-
-
- junit
- junit
- 4.1
- test
-
-
-
-
- cake-manager
-
-
-
- maven-compiler-plugin
- 2.3.2
-
- 1.8
- 1.8
-
-
-
-
- org.eclipse.jetty
- jetty-maven-plugin
-
- 10
- STOP
- 8005
-
- 8282
-
-
-
-
-
-
+
+
+
+ org.springframework.cloud
+ spring-cloud-contract-maven-plugin
+ 4.0.4
+ true
+
+ JUNIT5
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
diff --git a/src/main/java/com.waracle.cakemgr/CakeEntity.java b/src/main/java/com.waracle.cakemgr/CakeEntity.java
deleted file mode 100644
index 7927bd5d..00000000
--- a/src/main/java/com.waracle.cakemgr/CakeEntity.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.waracle.cakemgr;
-
-import java.io.Serializable;
-
-import javax.persistence.*;
-
-@Entity
-@org.hibernate.annotations.Entity(dynamicUpdate = true)
-@Table(name = "Employee", uniqueConstraints = {@UniqueConstraint(columnNames = "ID"), @UniqueConstraint(columnNames = "EMAIL")})
-public class CakeEntity implements Serializable {
-
- private static final long serialVersionUID = -1798070786993154676L;
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "ID", unique = true, nullable = false)
- private Integer employeeId;
-
- @Column(name = "EMAIL", unique = true, nullable = false, length = 100)
- private String title;
-
- @Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
- private String description;
-
- @Column(name = "LAST_NAME", unique = false, nullable = false, length = 300)
- private String image;
-
- 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 String getImage() {
- return image;
- }
-
- public void setImage(String image) {
- this.image = image;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/com.waracle.cakemgr/CakeServlet.java b/src/main/java/com.waracle.cakemgr/CakeServlet.java
deleted file mode 100644
index 9bd32f76..00000000
--- a/src/main/java/com.waracle.cakemgr/CakeServlet.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.waracle.cakemgr;
-
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonToken;
-import org.hibernate.Session;
-import org.hibernate.exception.ConstraintViolationException;
-
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.*;
-import java.net.URL;
-import java.util.List;
-
-@WebServlet("/cakes")
-public class CakeServlet extends HttpServlet {
-
- @Override
- public void init() throws ServletException {
- super.init();
-
- System.out.println("init started");
-
-
- System.out.println("downloading cake json");
- try (InputStream inputStream = new URL("https://gist.githubusercontent.com/hart88/198f29ec5114a3ec3460/raw/8dd19a88f9b8d24c23d9960f3300d0c917a4f07c/cake.json").openStream()) {
- BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
-
- StringBuffer buffer = new StringBuffer();
- String line = reader.readLine();
- while (line != null) {
- buffer.append(line);
- line = reader.readLine();
- }
-
- System.out.println("parsing cake json");
- JsonParser parser = new JsonFactory().createParser(buffer.toString());
- if (JsonToken.START_ARRAY != parser.nextToken()) {
- throw new Exception("bad token");
- }
-
- JsonToken nextToken = parser.nextToken();
- while(nextToken == JsonToken.START_OBJECT) {
- System.out.println("creating cake entity");
-
- CakeEntity cakeEntity = new CakeEntity();
- System.out.println(parser.nextFieldName());
- cakeEntity.setTitle(parser.nextTextValue());
-
- System.out.println(parser.nextFieldName());
- cakeEntity.setDescription(parser.nextTextValue());
-
- System.out.println(parser.nextFieldName());
- cakeEntity.setImage(parser.nextTextValue());
-
- Session session = HibernateUtil.getSessionFactory().openSession();
- try {
- session.beginTransaction();
- session.persist(cakeEntity);
- System.out.println("adding cake entity");
- session.getTransaction().commit();
- } catch (ConstraintViolationException ex) {
-
- }
- session.close();
-
- nextToken = parser.nextToken();
- System.out.println(nextToken);
-
- nextToken = parser.nextToken();
- System.out.println(nextToken);
- }
-
- } catch (Exception ex) {
- throw new ServletException(ex);
- }
-
- System.out.println("init finished");
- }
-
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-
- Session session = HibernateUtil.getSessionFactory().openSession();
- List list = session.createCriteria(CakeEntity.class).list();
-
- resp.getWriter().println("[");
-
- for (CakeEntity entity : list) {
- resp.getWriter().println("\t{");
-
- resp.getWriter().println("\t\t\"title\" : " + entity.getTitle() + ", ");
- resp.getWriter().println("\t\t\"desc\" : " + entity.getDescription() + ",");
- resp.getWriter().println("\t\t\"image\" : " + entity.getImage());
-
- resp.getWriter().println("\t}");
- }
-
- resp.getWriter().println("]");
-
- }
-
-}
diff --git a/src/main/java/com.waracle.cakemgr/HibernateUtil.java b/src/main/java/com.waracle.cakemgr/HibernateUtil.java
deleted file mode 100644
index 41ef137b..00000000
--- a/src/main/java/com.waracle.cakemgr/HibernateUtil.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.waracle.cakemgr;
-
-import org.hibernate.SessionFactory;
-import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
-import org.hibernate.cfg.Configuration;
-import org.hibernate.service.ServiceRegistry;
-
-public class HibernateUtil {
-
- private static SessionFactory sessionFactory = buildSessionFactory();
-
- private static SessionFactory buildSessionFactory() {
- try {
- if (sessionFactory == null) {
- Configuration configuration = new Configuration().configure(HibernateUtil.class.getResource("/hibernate.cfg.xml"));
- StandardServiceRegistryBuilder serviceRegistryBuilder = new StandardServiceRegistryBuilder();
- serviceRegistryBuilder.applySettings(configuration.getProperties());
- ServiceRegistry serviceRegistry = serviceRegistryBuilder.build();
- sessionFactory = configuration.buildSessionFactory(serviceRegistry);
- }
- return sessionFactory;
- } catch (Throwable ex) {
- System.err.println("Initial SessionFactory creation failed." + ex);
- throw new ExceptionInInitializerError(ex);
- }
- }
-
- public static SessionFactory getSessionFactory() {
- return sessionFactory;
- }
-
- public static void shutdown() {
- getSessionFactory().close();
- }
-
-}
diff --git a/src/main/java/com/philldenness/cakemanager/CakeManagerApplication.java b/src/main/java/com/philldenness/cakemanager/CakeManagerApplication.java
new file mode 100644
index 00000000..7a89e4a8
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/CakeManagerApplication.java
@@ -0,0 +1,13 @@
+package com.philldenness.cakemanager;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class CakeManagerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CakeManagerApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
new file mode 100644
index 00000000..ff5fd193
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
@@ -0,0 +1,21 @@
+package com.philldenness.cakemanager.controller;
+
+import java.util.List;
+
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.service.CakeService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+public class CakeController {
+
+ private final CakeService cakeService;
+
+ @GetMapping("/cakes")
+ public List getAllCakes() {
+ return cakeService.getCakes();
+ }
+}
diff --git a/src/main/java/com/philldenness/cakemanager/dto/CakeDTO.java b/src/main/java/com/philldenness/cakemanager/dto/CakeDTO.java
new file mode 100644
index 00000000..e9d98f96
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/dto/CakeDTO.java
@@ -0,0 +1,8 @@
+package com.philldenness.cakemanager.dto;
+
+public record CakeDTO(
+ String title,
+ String description,
+ String image
+) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java b/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
new file mode 100644
index 00000000..2175922e
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
@@ -0,0 +1,23 @@
+package com.philldenness.cakemanager.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "cake")
+public class CakeEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ private Long employeeId;
+ private String title;
+ private String description;
+ private String image;
+}
\ No newline at end of file
diff --git a/src/main/java/com/philldenness/cakemanager/mapper/CakeMapper.java b/src/main/java/com/philldenness/cakemanager/mapper/CakeMapper.java
new file mode 100644
index 00000000..2b2e39fd
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/mapper/CakeMapper.java
@@ -0,0 +1,12 @@
+package com.philldenness.cakemanager.mapper;
+
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.entity.CakeEntity;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CakeMapper {
+ public CakeDTO toDTO(CakeEntity entity) {
+ return new CakeDTO(entity.getTitle(), entity.getDescription(), entity.getImage());
+ }
+}
diff --git a/src/main/java/com/philldenness/cakemanager/repository/CakeRepository.java b/src/main/java/com/philldenness/cakemanager/repository/CakeRepository.java
new file mode 100644
index 00000000..7bf8f2e0
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/repository/CakeRepository.java
@@ -0,0 +1,9 @@
+package com.philldenness.cakemanager.repository;
+
+import com.philldenness.cakemanager.entity.CakeEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface CakeRepository extends JpaRepository {
+}
diff --git a/src/main/java/com/philldenness/cakemanager/service/CakeService.java b/src/main/java/com/philldenness/cakemanager/service/CakeService.java
new file mode 100644
index 00000000..ba444b19
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/service/CakeService.java
@@ -0,0 +1,20 @@
+package com.philldenness.cakemanager.service;
+
+import java.util.List;
+
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.mapper.CakeMapper;
+import com.philldenness.cakemanager.repository.CakeRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CakeService {
+ private final CakeRepository repository;
+ private final CakeMapper cakeMapper;
+
+ public List getCakes() {
+ return repository.findAll().stream().map(cakeMapper::toDTO).toList();
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 00000000..97c35704
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driverClassName=org.h2.Driver
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=none
diff --git a/src/main/resources/db/migration/V1__Initial.sql b/src/main/resources/db/migration/V1__Initial.sql
new file mode 100644
index 00000000..a967eb6a
--- /dev/null
+++ b/src/main/resources/db/migration/V1__Initial.sql
@@ -0,0 +1,8 @@
+CREATE TABLE cake
+(
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ employee_id INT,
+ title VARCHAR,
+ description VARCHAR,
+ image VARCHAR
+);
\ No newline at end of file
diff --git a/src/main/resources/hibernate.cfg.xml b/src/main/resources/hibernate.cfg.xml
deleted file mode 100644
index 0ae06d63..00000000
--- a/src/main/resources/hibernate.cfg.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- class,hbm
- org.hibernate.dialect.HSQLDialect
- true
- org.hsqldb.jdbcDriver
- sa
-
- jdbc:hsqldb:mem:db
- create
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
deleted file mode 100644
index d004447f..00000000
--- a/src/main/webapp/WEB-INF/web.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- Archetype Created Web Application
-
-
diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp
deleted file mode 100644
index c38169bb..00000000
--- a/src/main/webapp/index.jsp
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-Hello World!
-
-
diff --git a/src/test/java/com/philldenness/cakemanager/controller/CakeControllerTest.java b/src/test/java/com/philldenness/cakemanager/controller/CakeControllerTest.java
new file mode 100644
index 00000000..c276f56d
--- /dev/null
+++ b/src/test/java/com/philldenness/cakemanager/controller/CakeControllerTest.java
@@ -0,0 +1,34 @@
+package com.philldenness.cakemanager.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.service.CakeService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CakeControllerTest {
+
+ @Mock
+ private CakeService cakeService;
+
+ @InjectMocks
+ private CakeController cakeController;
+
+ @Test
+ void shouldGetCakeReturnsCakesFromCakeService() {
+ List cakes = List.of(new CakeDTO("title", "description", "image"));
+ when(cakeService.getCakes()).thenReturn(cakes);
+
+ List cakeList = cakeController.getAllCakes();
+
+ assertEquals(cakes, cakeList);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
new file mode 100644
index 00000000..61de86db
--- /dev/null
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
@@ -0,0 +1,45 @@
+package com.philldenness.cakemanager.integrationTest;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.philldenness.cakemanager.entity.CakeEntity;
+import com.philldenness.cakemanager.repository.CakeRepository;
+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;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class GetCakeIT {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private CakeRepository cakeRepository;
+
+ @BeforeEach
+ void setUp() {
+ CakeEntity cakeEntity = new CakeEntity();
+ cakeEntity.setEmployeeId(1L);
+ cakeEntity.setTitle("Lemon cheesecake");
+ cakeEntity.setDescription("A cheesecake made of lemon");
+ cakeEntity.setImage("https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg");
+
+ cakeRepository.save(cakeEntity);
+ }
+
+ @Test
+ void testGetCakesReturnsAllCakes() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/cakes").accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().json("[{'title':'Lemon cheesecake','description':'A cheesecake made of lemon','image':'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'}]"));
+
+ }
+}
diff --git a/src/test/java/com/philldenness/cakemanager/mapper/CakeMapperTest.java b/src/test/java/com/philldenness/cakemanager/mapper/CakeMapperTest.java
new file mode 100644
index 00000000..7950e4d8
--- /dev/null
+++ b/src/test/java/com/philldenness/cakemanager/mapper/CakeMapperTest.java
@@ -0,0 +1,31 @@
+package com.philldenness.cakemanager.mapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.entity.CakeEntity;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CakeMapperTest {
+
+ @InjectMocks
+ private CakeMapper cakeMapper;
+
+ @Test
+ void shouldConvertEntityToDTO() {
+ CakeEntity cakeEntity = new CakeEntity();
+ cakeEntity.setTitle("a title");
+ cakeEntity.setDescription("a description");
+ cakeEntity.setImage("an image");
+
+ CakeDTO cakeDTO = cakeMapper.toDTO(cakeEntity);
+
+ assertEquals(cakeEntity.getTitle(), cakeDTO.title());
+ assertEquals(cakeEntity.getDescription(), cakeDTO.description());
+ assertEquals(cakeEntity.getImage(), cakeDTO.image());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/philldenness/cakemanager/service/CakeServiceTest.java b/src/test/java/com/philldenness/cakemanager/service/CakeServiceTest.java
new file mode 100644
index 00000000..7a0421ca
--- /dev/null
+++ b/src/test/java/com/philldenness/cakemanager/service/CakeServiceTest.java
@@ -0,0 +1,47 @@
+package com.philldenness.cakemanager.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import com.philldenness.cakemanager.entity.CakeEntity;
+import com.philldenness.cakemanager.mapper.CakeMapper;
+import com.philldenness.cakemanager.dto.CakeDTO;
+import com.philldenness.cakemanager.repository.CakeRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CakeServiceTest {
+
+ @InjectMocks
+ private CakeService cakeService;
+
+ @Mock
+ private CakeRepository cakeRepository;
+
+ @Mock
+ private CakeMapper cakeMapper;
+
+ @Test
+ void shouldCallMapperWithEntityAndReturnMapperResult() {
+ CakeDTO cakeDTO1 = mock(CakeDTO.class);
+ CakeDTO cakeDTO2 = mock(CakeDTO.class);
+ CakeEntity cakeEntity1 = mock(CakeEntity.class);
+ CakeEntity cakeEntity2 = mock(CakeEntity.class);
+
+ when(cakeRepository.findAll()).thenReturn(List.of(cakeEntity1, cakeEntity2));
+ when(cakeMapper.toDTO(any(CakeEntity.class))).thenReturn(cakeDTO1).thenReturn(cakeDTO2);
+
+ List cakes = cakeService.getCakes();
+
+ verify(cakeMapper, times(1)).toDTO(cakeEntity1);
+ verify(cakeMapper, times(1)).toDTO(cakeEntity2);
+ assertEquals(List.of(cakeDTO1, cakeDTO2), cakes);
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/json/cakes-list.json b/src/test/resources/json/cakes-list.json
new file mode 100644
index 00000000..6e40dc24
--- /dev/null
+++ b/src/test/resources/json/cakes-list.json
@@ -0,0 +1,102 @@
+[
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ }
+]
\ No newline at end of file
From 645368fb80f5d4d8eb1e9e7888c30f4c71fafc91 Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Sat, 26 Aug 2023 12:58:47 +0100
Subject: [PATCH 02/16] Add swagger and seed data
---
README.txt | 3 +
pom.xml | 41 ++++---
.../controller/CakeController.java | 2 +
.../cakemanager/entity/CakeEntity.java | 1 -
.../resources/db/migration/V1__Initial.sql | 7 +-
.../db/migration/V2__insert_cakes.sql | 41 +++++++
.../integrationTest/GetCakeIT.java | 21 +---
src/test/resources/json/cakes-list.json | 102 ------------------
8 files changed, 80 insertions(+), 138 deletions(-)
create mode 100644 src/main/resources/db/migration/V2__insert_cakes.sql
delete mode 100644 src/test/resources/json/cakes-list.json
diff --git a/README.txt b/README.txt
index 593bc1eb..5e717065 100644
--- a/README.txt
+++ b/README.txt
@@ -65,5 +65,8 @@ If running in intelliJ it requires lombok annotations to be enabled.
I used TDD and 'The Double Loop' cycle which is described brilliantly here: https://jmauerhan.wordpress.com/talks/double-loop-tdd-bdd-done-right/
Flyway migration scripts run automatically on startup
+Build with integration tests command: mvn verify -Pfailsafe
+Swagger is on http://localhost:8080/swagger-ui/index.html
+
Next steps:
Add pagination and sorting to getAll endpoint
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 01bac950..a6f1c803 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,7 @@
20
2022.0.4
9.21.2
+ 2.2.0
@@ -36,6 +37,11 @@
flyway-core
${flywaydb-core.version}
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc-openapi.version}
+
org.projectlombok
lombok
@@ -46,11 +52,6 @@
spring-boot-starter-test
test
-
- org.springframework.cloud
- spring-cloud-starter-contract-verifier
- test
-
@@ -66,15 +67,6 @@
-
- org.springframework.cloud
- spring-cloud-contract-maven-plugin
- 4.0.4
- true
-
- JUNIT5
-
-
org.springframework.boot
spring-boot-maven-plugin
@@ -89,4 +81,25 @@
+
+
+ failsafe
+
+
+
+ maven-failsafe-plugin
+ 2.22.0
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
index ff5fd193..1832aeef 100644
--- a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
+++ b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
@@ -4,6 +4,7 @@
import com.philldenness.cakemanager.dto.CakeDTO;
import com.philldenness.cakemanager.service.CakeService;
+import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -15,6 +16,7 @@ public class CakeController {
private final CakeService cakeService;
@GetMapping("/cakes")
+ @Operation(summary = "Get all cakes")
public List getAllCakes() {
return cakeService.getCakes();
}
diff --git a/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java b/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
index 2175922e..39c787fc 100644
--- a/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
+++ b/src/main/java/com/philldenness/cakemanager/entity/CakeEntity.java
@@ -16,7 +16,6 @@ public class CakeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- private Long employeeId;
private String title;
private String description;
private String image;
diff --git a/src/main/resources/db/migration/V1__Initial.sql b/src/main/resources/db/migration/V1__Initial.sql
index a967eb6a..7442f112 100644
--- a/src/main/resources/db/migration/V1__Initial.sql
+++ b/src/main/resources/db/migration/V1__Initial.sql
@@ -1,8 +1,7 @@
CREATE TABLE cake
(
id INT AUTO_INCREMENT PRIMARY KEY,
- employee_id INT,
- title VARCHAR,
- description VARCHAR,
- image VARCHAR
+ title VARCHAR NOT NULL,
+ description VARCHAR NOT NULL,
+ image VARCHAR NOT NULL
);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V2__insert_cakes.sql b/src/main/resources/db/migration/V2__insert_cakes.sql
new file mode 100644
index 00000000..f305a8ee
--- /dev/null
+++ b/src/main/resources/db/migration/V2__insert_cakes.sql
@@ -0,0 +1,41 @@
+INSERT INTO cake (title, description, image)
+VALUES ('Lemon cheesecake', 'A cheesecake made of lemon',
+ 'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'),
+ ('victoria sponge', 'sponge with jam',
+ 'http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg'),
+ ('Carrot cake', 'Bugs bunnys favourite',
+ 'http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg'),
+ ('Banana cake', 'Donkey kongs favourite',
+ 'http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg'),
+ ('Birthday cake', 'a yearly treat',
+ 'http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg'),
+ ('Lemon cheesecake', 'A cheesecake made of lemon',
+ 'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'),
+ ('victoria sponge', 'sponge with jam',
+ 'http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg'),
+ ('Carrot cake', 'Bugs bunnys favourite',
+ 'http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg'),
+ ('Banana cake', 'Donkey kongs favourite',
+ 'http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg'),
+ ('Birthday cake', 'a yearly treat',
+ 'http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg'),
+ ('Lemon cheesecake', 'A cheesecake made of lemon',
+ 'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'),
+ ('victoria sponge', 'sponge with jam',
+ 'http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg'),
+ ('Carrot cake', 'Bugs bunnys favourite',
+ 'http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg'),
+ ('Banana cake', 'Donkey kongs favourite',
+ 'http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg'),
+ ('Birthday cake', 'a yearly treat',
+ 'http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg'),
+ ('Lemon cheesecake', 'A cheesecake made of lemon',
+ 'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'),
+ ('victoria sponge', 'sponge with jam',
+ 'http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg'),
+ ('Carrot cake', 'Bugs bunnys favourite',
+ 'http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg'),
+ ('Banana cake', 'Donkey kongs favourite',
+ 'http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg'),
+ ('Birthday cake', 'a yearly treat',
+ 'http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg');
\ No newline at end of file
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
index 61de86db..c0950a10 100644
--- a/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
@@ -1,11 +1,8 @@
package com.philldenness.cakemanager.integrationTest;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import com.philldenness.cakemanager.entity.CakeEntity;
import com.philldenness.cakemanager.repository.CakeRepository;
-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;
@@ -13,6 +10,7 @@
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;
@SpringBootTest
@AutoConfigureMockMvc
@@ -24,22 +22,11 @@ public class GetCakeIT {
@Autowired
private CakeRepository cakeRepository;
- @BeforeEach
- void setUp() {
- CakeEntity cakeEntity = new CakeEntity();
- cakeEntity.setEmployeeId(1L);
- cakeEntity.setTitle("Lemon cheesecake");
- cakeEntity.setDescription("A cheesecake made of lemon");
- cakeEntity.setImage("https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg");
-
- cakeRepository.save(cakeEntity);
- }
-
@Test
void testGetCakesReturnsAllCakes() throws Exception {
- mvc.perform(MockMvcRequestBuilders.get("/cakes").accept(MediaType.APPLICATION_JSON))
+ mvc.perform(MockMvcRequestBuilders.get("/cakes")
+ .accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
- .andExpect(content().json("[{'title':'Lemon cheesecake','description':'A cheesecake made of lemon','image':'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'}]"));
-
+ .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(20));
}
}
diff --git a/src/test/resources/json/cakes-list.json b/src/test/resources/json/cakes-list.json
deleted file mode 100644
index 6e40dc24..00000000
--- a/src/test/resources/json/cakes-list.json
+++ /dev/null
@@ -1,102 +0,0 @@
-[
- {
- "title": "Lemon cheesecake",
- "desc": "A cheesecake made of lemon",
- "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
- },
- {
- "title": "victoria sponge",
- "desc": "sponge with jam",
- "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
- },
- {
- "title": "Carrot cake",
- "desc": "Bugs bunnys favourite",
- "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
- },
- {
- "title": "Banana cake",
- "desc": "Donkey kongs favourite",
- "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
- },
- {
- "title": "Birthday cake",
- "desc": "a yearly treat",
- "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
- },
- {
- "title": "Lemon cheesecake",
- "desc": "A cheesecake made of lemon",
- "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
- },
- {
- "title": "victoria sponge",
- "desc": "sponge with jam",
- "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
- },
- {
- "title": "Carrot cake",
- "desc": "Bugs bunnys favourite",
- "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
- },
- {
- "title": "Banana cake",
- "desc": "Donkey kongs favourite",
- "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
- },
- {
- "title": "Birthday cake",
- "desc": "a yearly treat",
- "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
- },
- {
- "title": "Lemon cheesecake",
- "desc": "A cheesecake made of lemon",
- "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
- },
- {
- "title": "victoria sponge",
- "desc": "sponge with jam",
- "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
- },
- {
- "title": "Carrot cake",
- "desc": "Bugs bunnys favourite",
- "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
- },
- {
- "title": "Banana cake",
- "desc": "Donkey kongs favourite",
- "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
- },
- {
- "title": "Birthday cake",
- "desc": "a yearly treat",
- "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
- },
- {
- "title": "Lemon cheesecake",
- "desc": "A cheesecake made of lemon",
- "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
- },
- {
- "title": "victoria sponge",
- "desc": "sponge with jam",
- "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
- },
- {
- "title": "Carrot cake",
- "desc": "Bugs bunnys favourite",
- "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
- },
- {
- "title": "Banana cake",
- "desc": "Donkey kongs favourite",
- "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
- },
- {
- "title": "Birthday cake",
- "desc": "a yearly treat",
- "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
- }
-]
\ No newline at end of file
From 0a9352840a83ed7fb7c5d069e339b6f9323fc734 Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Sat, 26 Aug 2023 13:26:22 +0100
Subject: [PATCH 03/16] Add docker and docker-compose support
---
.dockerignore | 1 +
Dockerfile | 18 ++++++++++++++++++
README.txt | 16 +++++++++++++---
compose.yml | 6 ++++++
pom.xml | 21 ---------------------
5 files changed, 38 insertions(+), 24 deletions(-)
create mode 100644 .dockerignore
create mode 100644 Dockerfile
create mode 100644 compose.yml
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..1de56593
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+target
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..5044cb68
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+# syntax=docker/dockerfile:1
+
+FROM maven:3.9.3-amazoncorretto-20 AS builder
+WORKDIR /app
+COPY pom.xml ./
+COPY src ./src
+
+RUN mvn clean install
+
+# Second stage: Minimal runtime environment
+FROM eclipse-temurin:20-jre-jammy
+WORKDIR /app
+
+# copy jar from the first stage
+COPY --from=builder /app/target/*.jar /app/app.jar
+
+EXPOSE 8080
+ENTRYPOINT ["java", "-jar", "/app/app.jar"]
\ No newline at end of file
diff --git a/README.txt b/README.txt
index 5e717065..95a0845e 100644
--- a/README.txt
+++ b/README.txt
@@ -60,12 +60,22 @@ Good luck!
Notes from Phill
==========
-Requires Java 20 to run.
-If running in intelliJ it requires lombok annotations to be enabled.
+Requires Java 20 to run and requires lombok annotations to be enabled on intelliJ.
I used TDD and 'The Double Loop' cycle which is described brilliantly here: https://jmauerhan.wordpress.com/talks/double-loop-tdd-bdd-done-right/
Flyway migration scripts run automatically on startup
-Build with integration tests command: mvn verify -Pfailsafe
+Starting app
+Using docker
+Build the container ` docker build --tag java-docker . `
+Run the container `docker run --publish 8080:8080 java-docker`
+
+Using docker-compose
+run docker compose `docker-compose up`
+
+Using mvn
+run `mvn spring-boot:run`
+
+Documentation
Swagger is on http://localhost:8080/swagger-ui/index.html
Next steps:
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 00000000..5e74d65e
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,6 @@
+services:
+ backend:
+ container_name: java-docker
+ build: .
+ ports:
+ - 8080:8080
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a6f1c803..7a16c12c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,25 +81,4 @@
-
-
- failsafe
-
-
-
- maven-failsafe-plugin
- 2.22.0
-
-
-
- integration-test
- verify
-
-
-
-
-
-
-
-
From 237032b11bdcacc47177d111e3e25e3e672e494f Mon Sep 17 00:00:00 2001
From: Phillip Denness
Date: Sat, 26 Aug 2023 13:35:29 +0100
Subject: [PATCH 04/16] dockerhub cicd
---
.github/workflows/main.yml | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 .github/workflows/main.yml
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..5bb6efb6
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,31 @@
+name: ci
+
+on:
+ push:
+ branches:
+ - "master"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Checkout
+ uses: actions/checkout@v3
+ -
+ name: Login to Docker Hub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ -
+ name: Build and push
+ uses: docker/build-push-action@v4
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/clockbox:latest
From dbdb24fa91f7e382a7e3e1a3d1677296aedf6d66 Mon Sep 17 00:00:00 2001
From: Phillip Denness
Date: Sat, 26 Aug 2023 13:38:10 +0100
Subject: [PATCH 05/16] Tag docker as cakemanager
---
.github/workflows/main.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5bb6efb6..2488bc60 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -28,4 +28,4 @@ jobs:
context: .
file: ./Dockerfile
push: true
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/clockbox:latest
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/cakemanager:latest
From 74c2ef6fc259f7a37965bfc8547931bdd8b3576a Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Sat, 26 Aug 2023 13:46:46 +0100
Subject: [PATCH 06/16] Include readme for getting container from dockerhub
---
README.txt | 17 ++++++++++++-----
compose.yml | 2 +-
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/README.txt b/README.txt
index 95a0845e..271b3dee 100644
--- a/README.txt
+++ b/README.txt
@@ -62,12 +62,18 @@ Notes from Phill
Requires Java 20 to run and requires lombok annotations to be enabled on intelliJ.
I used TDD and 'The Double Loop' cycle which is described brilliantly here: https://jmauerhan.wordpress.com/talks/double-loop-tdd-bdd-done-right/
-Flyway migration scripts run automatically on startup
+Flyway migration scripts run automatically on startup.
+
+CICD
+Github actions workflow is setup to automatically test and build the docker container. Then it is pushed to dockerhub which can be viewed here: https://hub.docker.com/r/phillipdenness1/cakemanager
Starting app
-Using docker
-Build the container ` docker build --tag java-docker . `
-Run the container `docker run --publish 8080:8080 java-docker`
+Pull from dockerhub
+docker run --publish 8080:8080 phillipdenness1/cakemanager:latest
+
+Build and run docker
+Build the container `docker build --tag cakemanagerpd . `
+Run the container `docker run --publish 8080:8080 cakemanagerpd`
Using docker-compose
run docker compose `docker-compose up`
@@ -79,4 +85,5 @@ Documentation
Swagger is on http://localhost:8080/swagger-ui/index.html
Next steps:
-Add pagination and sorting to getAll endpoint
\ No newline at end of file
+Add pagination and sorting to getAll endpoint
+Extend CICD to deploy to cloud
\ No newline at end of file
diff --git a/compose.yml b/compose.yml
index 5e74d65e..32378223 100644
--- a/compose.yml
+++ b/compose.yml
@@ -1,6 +1,6 @@
services:
backend:
- container_name: java-docker
+ container_name: cakemanagerpd
build: .
ports:
- 8080:8080
\ No newline at end of file
From e1f75f5ba11e55132fc305a89fdd0faf0db413aa Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Sat, 26 Aug 2023 14:48:35 +0100
Subject: [PATCH 07/16] Get by ID happy and unhappy path
---
.../controller/CakeController.java | 10 ++++-
.../ControllerExceptionHandler.java | 15 +++++++
.../cakemanager/service/CakeService.java | 6 +++
.../controller/CakeControllerTest.java | 27 +++++++++++-
.../integrationTest/GetCakeIT.java | 20 +++++++--
.../cakemanager/service/CakeServiceTest.java | 44 +++++++++++++++++--
6 files changed, 113 insertions(+), 9 deletions(-)
create mode 100644 src/main/java/com/philldenness/cakemanager/controller/ControllerExceptionHandler.java
diff --git a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
index 1832aeef..d5d07c6e 100644
--- a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
+++ b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
@@ -7,17 +7,25 @@
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
+@RequestMapping("/cakes")
@RequiredArgsConstructor
public class CakeController {
private final CakeService cakeService;
- @GetMapping("/cakes")
+ @GetMapping
@Operation(summary = "Get all cakes")
public List getAllCakes() {
return cakeService.getCakes();
}
+
+ @GetMapping("/{id}")
+ public CakeDTO getCakeById(@PathVariable Long id) {
+ return cakeService.getCakeById(id);
+ }
}
diff --git a/src/main/java/com/philldenness/cakemanager/controller/ControllerExceptionHandler.java b/src/main/java/com/philldenness/cakemanager/controller/ControllerExceptionHandler.java
new file mode 100644
index 00000000..e84ae2dc
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/controller/ControllerExceptionHandler.java
@@ -0,0 +1,15 @@
+package com.philldenness.cakemanager.controller;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+@ControllerAdvice
+public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler({ IllegalArgumentException.class })
+ public ResponseEntity
## Methodology
I used TDD and 'The Double Loop' cycle which is described brilliantly here: https://jmauerhan.wordpress.com/talks/double-loop-tdd-bdd-done-right/
-
## CICD
* Github actions workflow is setup to automatically test and build the docker container. Then it is pushed to dockerhub which can be viewed here: https://hub.docker.com/r/phillipdenness1/cakemanager
-* Flyway migration scripts run automatically on startup.
+* Flyway migration scripts run automatically on startup and populates the H2 DB with test data.
---
## Starting app
@@ -77,16 +77,22 @@ mvn spring-boot:run
### Swagger documentation
- http://localhost:8080/swagger-ui/index.html
+ - Endpoints are validated jakarta.validation annotations
+
### Postman export
- postman.cakemanager.json
## Monitoring
* Logback with Logstash encoder to support searchable logs
* Prometheus is available when running via docker-compose.
- - Prometheus UI: http://localhost:9090/targets?search=
+ - Prometheus UI: http://localhost:9090/graph?
- find_all_counter_total, find_by_id_counter_total, save_counter_total, update_counter_total, delete_counter_total, unknown_error_counter_total, bad_request_counter_total
+## Authentication
+* Basic authentication is applied to secure endpoints
+ - Add header: `Authorization: Basic dXNlcjp1c2VyUGFzcw==`
+ - Username=user , password=userPass
+
## Next steps
* Add pagination and sorting to getAll endpoint
-* Extend CICD to deploy to cloud
-* Authentication and Authorisation
\ No newline at end of file
+* Extend CICD to deploy to cloud
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index d6b00a70..c06b8a27 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,10 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-security
+
io.micrometer
micrometer-registry-prometheus
@@ -80,6 +84,11 @@
spring-boot-starter-test
test
+
+ org.springframework.security
+ spring-security-test
+ test
+
diff --git a/postman.cakemanager.json b/postman.cakemanager.json
index f3999e5f..64154236 100644
--- a/postman.cakemanager.json
+++ b/postman.cakemanager.json
@@ -9,16 +9,32 @@
{
"name": "Get all cakes",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ },
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ }
+ ]
+ },
"method": "GET",
"header": [],
"url": {
- "raw": "http://localhost:8080/cakes",
+ "raw": "http://localhost:8080/api/cakes",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
+ "api",
"cakes"
]
}
@@ -28,6 +44,21 @@
{
"name": "Create cake",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ }
+ ]
+ },
"method": "POST",
"header": [],
"body": {
@@ -40,13 +71,14 @@
}
},
"url": {
- "raw": "http://localhost:8080/cakes",
+ "raw": "http://localhost:8080/api/cakes",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
+ "api",
"cakes"
]
}
@@ -56,6 +88,21 @@
{
"name": "Update cake",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ }
+ ]
+ },
"method": "PUT",
"header": [],
"body": {
@@ -68,13 +115,14 @@
}
},
"url": {
- "raw": "http://localhost:8080/cakes/2",
+ "raw": "http://localhost:8080/api/cakes/2",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
+ "api",
"cakes",
"2"
]
@@ -85,16 +133,32 @@
{
"name": "Delete cake",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ }
+ ]
+ },
"method": "DELETE",
"header": [],
"url": {
- "raw": "http://localhost:8080/cakes/2",
+ "raw": "http://localhost:8080/api/cakes/2",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
+ "api",
"cakes",
"2"
]
@@ -105,18 +169,34 @@
{
"name": "Get cake by ID",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ },
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ }
+ ]
+ },
"method": "GET",
"header": [],
"url": {
- "raw": "http://localhost:8080/cakes/2",
+ "raw": "http://localhost:8080/api/cakes/3",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
+ "api",
"cakes",
- "2"
+ "3"
]
}
},
@@ -125,6 +205,9 @@
{
"name": "health",
"request": {
+ "auth": {
+ "type": "noauth"
+ },
"method": "GET",
"header": [],
"url": {
@@ -145,6 +228,21 @@
{
"name": "info",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ }
+ ]
+ },
"method": "GET",
"header": [],
"url": {
@@ -165,6 +263,21 @@
{
"name": "prometheus",
"request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "username",
+ "value": "user",
+ "type": "string"
+ },
+ {
+ "key": "password",
+ "value": "userPass",
+ "type": "string"
+ }
+ ]
+ },
"method": "GET",
"header": [],
"url": {
diff --git a/prometheus.yml b/prometheus.yml
index e40efc0f..55474849 100644
--- a/prometheus.yml
+++ b/prometheus.yml
@@ -12,4 +12,7 @@ scrape_configs:
scrape_interval: 5s
static_configs:
- targets:
- - cakemanagerpd:8080
\ No newline at end of file
+ - cakemanagerpd:8080
+ basic_auth:
+ username: 'user'
+ password: 'userPass'
\ No newline at end of file
diff --git a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
index d421792b..170e60e3 100644
--- a/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
+++ b/src/main/java/com/philldenness/cakemanager/controller/CakeController.java
@@ -24,7 +24,7 @@
import org.springframework.web.bind.annotation.RestController;
@RestController
-@RequestMapping("/cakes")
+@RequestMapping("/api/cakes")
@RequiredArgsConstructor
public class CakeController {
diff --git a/src/main/java/com/philldenness/cakemanager/dto/CakeRequest.java b/src/main/java/com/philldenness/cakemanager/dto/CakeRequest.java
index 8a976203..a8eb2a8a 100644
--- a/src/main/java/com/philldenness/cakemanager/dto/CakeRequest.java
+++ b/src/main/java/com/philldenness/cakemanager/dto/CakeRequest.java
@@ -1,10 +1,10 @@
package com.philldenness.cakemanager.dto;
-import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.NotBlank;
public record CakeRequest(
- @NotNull String title,
- @NotNull String description,
- @NotNull String image
+ @NotBlank String title,
+ @NotBlank String description,
+ @NotBlank String image
) {
}
\ No newline at end of file
diff --git a/src/main/java/com/philldenness/cakemanager/security/BasicConfiguration.java b/src/main/java/com/philldenness/cakemanager/security/BasicConfiguration.java
new file mode 100644
index 00000000..c4b8cd5d
--- /dev/null
+++ b/src/main/java/com/philldenness/cakemanager/security/BasicConfiguration.java
@@ -0,0 +1,49 @@
+package com.philldenness.cakemanager.security;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class BasicConfiguration {
+
+ private static final String[] AUTH_WHITELIST = {
+ "/v3/api-docs/**",
+ "/swagger-ui/**",
+ "/actuator/health",
+ };
+
+ @Bean
+ public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
+ UserDetails user = User.withUsername("user")
+ .password(passwordEncoder.encode("userPass"))
+ .roles("USER")
+ .build();
+
+ return new InMemoryUserDetailsManager(user);
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http.csrf().disable().
+ authorizeRequests()
+ .requestMatchers(AUTH_WHITELIST)
+ .permitAll()
+ .anyRequest()
+ .authenticated()
+ .and()
+ .httpBasic();
+ return http.build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return PasswordEncoderFactories.createDelegatingPasswordEncoder();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 03083664..92b9b996 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -5,4 +5,5 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
management.endpoints.web.exposure.include=health,info,prometheus
-management.metrics.tags.application=${spring.application.name}
\ No newline at end of file
+management.metrics.tags.application=${spring.application.name}
+spring.profiles.active=workspace
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index e67af387..a2090786 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -1,8 +1,20 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/CreateCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/CreateCakeIT.java
index b9adb449..de61b50a 100644
--- a/src/test/java/com/philldenness/cakemanager/integrationTest/CreateCakeIT.java
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/CreateCakeIT.java
@@ -10,19 +10,21 @@
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@SpringBootTest
@AutoConfigureMockMvc
+@WithMockUser
public class CreateCakeIT {
@Autowired
private MockMvc mvc;
@Test
- void testValidPostCreatesCake() throws Exception {
- mvc.perform(MockMvcRequestBuilders.post("/cakes")
+ void testCreateCake_returnsCake() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
.content(asJsonString(new CakeRequest("new title", "new description", "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
@@ -32,28 +34,55 @@ void testValidPostCreatesCake() throws Exception {
@Test
void testInvalidPostCreatesCakeWithNullTitle() throws Exception {
- mvc.perform(MockMvcRequestBuilders.post("/cakes")
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
.content(asJsonString(new CakeRequest(null, "new description", "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+ @Test
+ void testInvalidPostCreatesCakeWithBlankTitle() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
+ .content(asJsonString(new CakeRequest("", "new description", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
+
@Test
void testInvalidPostCreatesCakeWithNullDescription() throws Exception {
- mvc.perform(MockMvcRequestBuilders.post("/cakes")
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
.content(asJsonString(new CakeRequest("new title", null, "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+ @Test
+ void testInvalidPostCreatesCakeWithBlankDescription() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
+ .content(asJsonString(new CakeRequest("new title", "", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
+
@Test
void testInvalidPostCreatesCakeWithNullImage() throws Exception {
- mvc.perform(MockMvcRequestBuilders.post("/cakes")
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
.content(asJsonString(new CakeRequest("new title", "new description", null)))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+
+ @Test
+ void testInvalidPostCreatesCakeWithBlankImage() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.post("/api/cakes")
+ .content(asJsonString(new CakeRequest("new title", "new description", "")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
}
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/DeleteCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/DeleteCakeIT.java
index f49b8472..37b4b43d 100644
--- a/src/test/java/com/philldenness/cakemanager/integrationTest/DeleteCakeIT.java
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/DeleteCakeIT.java
@@ -9,11 +9,13 @@
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@SpringBootTest
@AutoConfigureMockMvc
+@WithMockUser
public class DeleteCakeIT {
@Autowired
@@ -30,14 +32,14 @@ void testDeleteCake() throws Exception {
cakeEntity.setDescription("to delete");
CakeEntity deleteEntity = cakeRepository.save(cakeEntity);
- mvc.perform(MockMvcRequestBuilders.delete("/cakes/" + deleteEntity.getId())
+ mvc.perform(MockMvcRequestBuilders.delete("/api/cakes/" + deleteEntity.getId())
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent());
}
@Test
void testDeleteNonExistentCake() throws Exception {
- mvc.perform(MockMvcRequestBuilders.delete("/cakes/99")
+ mvc.perform(MockMvcRequestBuilders.delete("/api/cakes/99")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
index abee6ca9..d87ebdbc 100644
--- a/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/GetCakeIT.java
@@ -8,12 +8,14 @@
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc
+@WithMockUser
public class GetCakeIT {
@Autowired
@@ -21,8 +23,8 @@ public class GetCakeIT {
// region get all
@Test
- void testGetCakesReturnsAllCakes() throws Exception {
- mvc.perform(MockMvcRequestBuilders.get("/cakes")
+ void testGetCakes_returnsAllCakes() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/api/cakes")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(20));
@@ -31,23 +33,23 @@ void testGetCakesReturnsAllCakes() throws Exception {
// region get by id
@Test
- void testGetCakeById() throws Exception {
- mvc.perform(MockMvcRequestBuilders.get("/cakes/1")
+ void testGetCakeById_returnsCake() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/api/cakes/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json("{'title': 'Lemon cheesecake', 'description': 'A cheesecake made of lemon', 'image': 'https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg'}"));
}
@Test
- void testGetCakeByIdReturns404WhenIdDoesntExist() throws Exception {
- mvc.perform(MockMvcRequestBuilders.get("/cakes/99")
+ void testGetCakeById_returns404WhenIdDoesntExist() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/api/cakes/99")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
@Test
- void testGetCakeByIdReturns400WhenIdIsNotALong() throws Exception {
- mvc.perform(MockMvcRequestBuilders.get("/cakes/abc")
+ void testGetCakeById_returns400WhenIdIsNotALong() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.get("/api/cakes/abc")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/SecurityIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/SecurityIT.java
new file mode 100644
index 00000000..c1f67591
--- /dev/null
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/SecurityIT.java
@@ -0,0 +1,156 @@
+package com.philldenness.cakemanager.integrationTest;
+
+import static com.philldenness.cakemanager.testUtils.TestUtils.asJsonString;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+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;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.philldenness.cakemanager.dto.CakeRequest;
+import com.philldenness.cakemanager.entity.CakeEntity;
+import com.philldenness.cakemanager.repository.CakeRepository;
+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;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class SecurityIT {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private CakeRepository cakeRepository;
+
+ @Test
+ void testHealthWithUser_isOk() throws Exception {
+ mvc.perform(get("/actuator/health")
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
+ }
+
+ @Test
+ void testInfoWithUser_isOk() throws Exception {
+ mvc.perform(get("/actuator/info")
+ .with(httpBasic("user", "userPass"))
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
+ }
+
+ @Test
+ void testInfoWithUser_isUnauthorised() throws Exception {
+ mvc.perform(get("/actuator/info")
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void testCreateCakeWithUser_returnsCreated() throws Exception {
+ mvc.perform(post("/api/cakes")
+ .with(httpBasic("user", "userPass"))
+ .content(asJsonString(new CakeRequest("new title", "new description", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated());
+ }
+
+ @Test
+ void testCreateCakeWithoutUser_returnsUnauthorised() throws Exception {
+ mvc.perform(post("/api/cakes")
+ .content(asJsonString(new CakeRequest("new title", "new description", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void testDeleteCakeWithUser_returnsNoContent() throws Exception {
+ CakeEntity cakeEntity = new CakeEntity();
+ cakeEntity.setImage("to delete");
+ cakeEntity.setTitle("to delete");
+ cakeEntity.setDescription("to delete");
+ CakeEntity deleteEntity = cakeRepository.save(cakeEntity);
+
+ mvc.perform(delete("/api/cakes/" + deleteEntity.getId())
+ .with(httpBasic("user", "userPass"))
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ void testDeleteCakeWithoutUser_returnsUnauthorised() throws Exception {
+ CakeEntity cakeEntity = new CakeEntity();
+ cakeEntity.setImage("to delete");
+ cakeEntity.setTitle("to delete");
+ cakeEntity.setDescription("to delete");
+ CakeEntity deleteEntity = cakeRepository.save(cakeEntity);
+
+ mvc.perform(delete("/api/cakes/" + deleteEntity.getId())
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void testGetCakeByIdWithUser_returnsOk() throws Exception {
+ mvc.perform(get("/api/cakes/1")
+ .with(httpBasic("user", "userPass"))
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
+ }
+
+ @Test
+ void testGetCakeByIdWithoutUser_returnsUnauthorised() throws Exception {
+ mvc.perform(get("/api/cakes/1")
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void testGetCakesWithUser_returnsOk() throws Exception {
+ mvc.perform(get("/api/cakes")
+ .with(httpBasic("user", "userPass"))
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
+ }
+
+ @Test
+ void testGetCakesWithoutUser_returnsUnauthorised() throws Exception {
+ mvc.perform(get("/api/cakes")
+ .accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void testUpdateCakeWithUser_returnsOk() throws Exception {
+ CakeEntity existingEntity = new CakeEntity();
+ existingEntity.setTitle("old title");
+ existingEntity.setDescription("old desc");
+ existingEntity.setImage("old image");
+ existingEntity = cakeRepository.save(existingEntity);
+
+ CakeRequest request = new CakeRequest("new title", "new description", "new image");
+
+ mvc.perform(put("/api/cakes/" + existingEntity.getId())
+ .with(httpBasic("user", "userPass"))
+ .content(asJsonString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void testUpdateCakeWithoutUser_returnsUnauthorised() throws Exception {
+ CakeEntity existingEntity = new CakeEntity();
+ existingEntity.setTitle("old title");
+ existingEntity.setDescription("old desc");
+ existingEntity.setImage("old image");
+ existingEntity = cakeRepository.save(existingEntity);
+
+ CakeRequest request = new CakeRequest("new title", "new description", "new image");
+
+ mvc.perform(put("/api/cakes/" + existingEntity.getId())
+ .content(asJsonString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnauthorized());
+ }
+}
diff --git a/src/test/java/com/philldenness/cakemanager/integrationTest/UpdateCakeIT.java b/src/test/java/com/philldenness/cakemanager/integrationTest/UpdateCakeIT.java
index ae3228f0..3336333d 100644
--- a/src/test/java/com/philldenness/cakemanager/integrationTest/UpdateCakeIT.java
+++ b/src/test/java/com/philldenness/cakemanager/integrationTest/UpdateCakeIT.java
@@ -13,11 +13,13 @@
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@SpringBootTest
@AutoConfigureMockMvc
+@WithMockUser
public class UpdateCakeIT {
@Autowired
@@ -36,7 +38,7 @@ void testValidUpdateCake() throws Exception {
CakeRequest request = new CakeRequest("new title", "new description", "new image");
- mvc.perform(MockMvcRequestBuilders.put("/cakes/" + existingEntity.getId())
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/" + existingEntity.getId())
.content(asJsonString(request))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
@@ -51,7 +53,7 @@ void testValidUpdateCake() throws Exception {
@Test
void testUpdateCakeReturns404WhenIdDoesntExist() throws Exception {
- mvc.perform(MockMvcRequestBuilders.put("/cakes/99")
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/99")
.content(asJsonString(new CakeRequest("new title", "new description", "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
@@ -60,28 +62,55 @@ void testUpdateCakeReturns404WhenIdDoesntExist() throws Exception {
@Test
void testInvalidPostCreatesCakeWithNullTitle() throws Exception {
- mvc.perform(MockMvcRequestBuilders.put("/cakes/1")
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
.content(asJsonString(new CakeRequest(null, "new description", "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+ @Test
+ void testInvalidPostCreatesCakeWithBlankTitle() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
+ .content(asJsonString(new CakeRequest("", "new description", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
+
@Test
void testInvalidPostCreatesCakeWithNullDescription() throws Exception {
- mvc.perform(MockMvcRequestBuilders.put("/cakes/1")
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
.content(asJsonString(new CakeRequest("new title", null, "new image")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+ @Test
+ void testInvalidPostCreatesCakeWithBlankDescription() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
+ .content(asJsonString(new CakeRequest("new title", "", "new image")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
+
@Test
void testInvalidPostCreatesCakeWithNullImage() throws Exception {
- mvc.perform(MockMvcRequestBuilders.put("/cakes/1")
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
.content(asJsonString(new CakeRequest("new title", "new description", null)))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
+
+ @Test
+ void testInvalidPostCreatesCakeWithBlankImage() throws Exception {
+ mvc.perform(MockMvcRequestBuilders.put("/api/cakes/1")
+ .content(asJsonString(new CakeRequest("new title", "new description", "")))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest());
+ }
}
From 0e5f19468df37039a74f01f9b95a6880298bcd17 Mon Sep 17 00:00:00 2001
From: phillipDenness
Date: Mon, 28 Aug 2023 14:31:11 +0100
Subject: [PATCH 16/16] Exclude vulnerability
---
pom.xml | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/pom.xml b/pom.xml
index c06b8a27..cb8de50b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,10 @@
org.springframework.boot
spring-boot-starter-logging
+
+ org.yaml
+ snakeyaml
+
@@ -46,6 +50,7 @@
com.h2database
h2
+ 2.2.220
runtime
@@ -57,6 +62,12 @@
org.springdoc
springdoc-openapi-starter-webmvc-ui
${springdoc-openapi.version}
+
+
+ org.yaml
+ snakeyaml
+
+
org.springframework.boot