Account REST API exercise 💪
+Improve your *Spring MVC* configuration and rest mapping skills
+### Task
+This webapp provides a **simple REST API for `Account`**. The data is stored using in-memory fake DAO. Your job is to
+**configure Spring MVC application** and **implement AccountRestController**. In order to complete the task, please
+**follow the instructions in the *todo* section**
+
+To verify your configuration, run `AccountRestControllerTest.java` :white_check_mark:
+
+
+### Pre-conditions ❗
+You're supposed to be familiar with *Spring MVC*
+
+### How to start ❓
+* Just clone the repository and start implementing the **todo** section, verify your changes by running tests
+* If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source)
+* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation
+
+### Related materials ℹ
+ * todo
+
diff --git a/3-0-spring-framework/3-2-1-account-rest-api/pom.xml b/3-0-spring-framework/3-2-1-account-rest-api/pom.xml
new file mode 100644
index 0000000..7ec0e00
--- /dev/null
+++ b/3-0-spring-framework/3-2-1-account-rest-api/pom.xml
@@ -0,0 +1,57 @@
+
++ * todo: 1. Mark this class as config + * todo: 2. Enable component scanning for all packages in "com.bobocode" using annotation property "basePackages" + * todo: 3. Exclude web related config and beans (ignore @{@link Controller}, ignore {@link EnableWebMvc}) + */ +public class RootConfig { +} diff --git a/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/config/WebConfig.java b/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/config/WebConfig.java new file mode 100644 index 0000000..1197302 --- /dev/null +++ b/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/config/WebConfig.java @@ -0,0 +1,11 @@ +package com.bobocode.config; + +/** + * This class provides web (servlet) related configuration. + *
+ * todo: 1. Mark this class as Spring config class
+ * todo: 2. Enable web mvc using annotation
+ * todo: 3. Enable component scanning for package "web" using annotation value
+ */
+public class WebConfig {
+}
diff --git a/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/dao/AccountDao.java b/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/dao/AccountDao.java
new file mode 100644
index 0000000..0ac5d36
--- /dev/null
+++ b/3-0-spring-framework/3-2-1-account-rest-api/src/main/java/com/bobocode/dao/AccountDao.java
@@ -0,0 +1,15 @@
+package com.bobocode.dao;
+
+import com.bobocode.model.Account;
+
+import java.util.List;
+
+public interface AccountDao {
+ List
+ * todo: 1. Configure a component with name "accountDao"
+ */
+public class InMemoryAccountDao implements AccountDao {
+ private Map
+ * todo: 1. Configure rest controller that handles requests with url "/accounts"
+ * todo: 2. Inject {@link AccountDao} implementation
+ * todo: 3. Implement method that handles GET request and returns a list of accounts
+ * todo: 4. Implement method that handles GET request with id as path variable and returns account by id
+ * todo: 5. Implement method that handles POST request, receives account as request body, saves account and returns it
+ * todo: Configure HTTP response status code 201 - CREATED
+ * todo: 6. Implement method that handles PUT request with id as path variable and receives account as request body.
+ * todo: It check if account id and path variable are the same and throws {@link IllegalStateException} otherwise.
+ * todo: Then it saves received account. Configure HTTP response status code 204 - NO CONTENT
+ * todo: 7. Implement method that handles DELETE request with id as path variable removes an account by id
+ * todo: Configure HTTP response status code 204 - NO CONTENT
+ */
+public class AccountRestController {
+
+}
diff --git a/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/AccountRestControllerTest.java b/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/AccountRestControllerTest.java
new file mode 100644
index 0000000..78ee089
--- /dev/null
+++ b/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/AccountRestControllerTest.java
@@ -0,0 +1,164 @@
+package com.bobocode;
+
+import com.bobocode.config.RootConfig;
+import com.bobocode.config.WebConfig;
+import com.bobocode.dao.AccountDao;
+import com.bobocode.dao.impl.InMemoryAccountDao;
+import com.bobocode.model.Account;
+import com.bobocode.web.controller.AccountRestController;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.lang.reflect.Constructor;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.hamcrest.Matchers.hasItems;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@SpringJUnitWebConfig(classes = {RootConfig.class, WebConfig.class})
+class AccountRestControllerTest {
+ @Autowired
+ private WebApplicationContext applicationContext;
+
+ @Autowired
+ private InMemoryAccountDao accountDao;
+
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setup() {
+ mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
+ accountDao.clear();
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("AccountRestController is marked as @RestController")
+ void accountRestControllerAnnotation() {
+ RestController restController = AccountRestController.class.getAnnotation(RestController.class);
+
+ assertNotNull(restController);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("AccountRestController is specified in @RequestMapping")
+ void accountRestControllerRequestMapping() {
+ RequestMapping requestMapping = AccountRestController.class.getAnnotation(RequestMapping.class);
+
+ assertNotNull(requestMapping);
+ assertThat(requestMapping.value().length).isEqualTo(1);
+ assertThat(requestMapping.value()).contains("/accounts");
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("AccountDao is injected using constructor")
+ void accountDaoInjection() {
+ Constructor> constructor = AccountRestController.class.getConstructors()[0];
+
+ assertThat(constructor.getParameterTypes()).contains(AccountDao.class);
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("Getting all accounts is implemented")
+ void getAllAccounts() throws Exception {
+ Account account1 = create("Johnny", "Boy", "jboy@gmail.com");
+ Account account2 = create("Okko", "Bay", "obay@gmail.com");
+ accountDao.save(account1);
+ accountDao.save(account2);
+
+ mockMvc.perform(get("/accounts"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.[*].email").value(hasItems("jboy@gmail.com", "obay@gmail.com")));
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("Getting all accounts response status is OK")
+ void getAccountsResponseStatusCode() throws Exception {
+ mockMvc.perform(get("/accounts").accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("Getting account by Id with path variable is implemented")
+ void getById() throws Exception {
+ Account account = create("Johnny", "Boy", "jboy@gmail.com");
+ accountDao.save(account);
+
+ mockMvc.perform(get(String.format("/accounts/%d", account.getId())))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(account.getId()))
+ .andExpect(jsonPath("$.email").value("jboy@gmail.com"))
+ .andExpect(jsonPath("$.firstName").value("Johnny"))
+ .andExpect(jsonPath("$.lastName").value("Boy"));
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("Creating account returns corresponding HTTP status - 201")
+ void httpStatusCodeOnCreate() throws Exception {
+ mockMvc.perform(
+ post("/accounts")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"jboy@gmail.com\"}"))
+ .andExpect(status().isCreated());
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("Creating account returns assigned Id")
+ void createAccountReturnsAssignedId() throws Exception {
+ mockMvc.perform(
+ post("/accounts")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"jboy@gmail.com\"}"))
+ .andExpect(jsonPath("$.id").value(1L));
+ }
+
+ private Account create(String firstName, String lastName, String email) {
+ Account account = new Account();
+ account.setFirstName(firstName);
+ account.setLastName(lastName);
+ account.setEmail(email);
+ return account;
+ }
+
+ @Test
+ @Order(9)
+ @DisplayName("Updating account is implemented")
+ void updateAccount() throws Exception {
+ Account account = create("Johnny", "Boy", "jboy@gmail.com");
+ accountDao.save(account);
+
+ mockMvc.perform(put(String.format("/accounts/%d", account.getId())).contentType(MediaType.APPLICATION_JSON)
+ .content(String.format("{\"id\":\"%d\", \"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"johnny.boy@gmail.com\"}", account.getId())))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @Order(10)
+ @DisplayName("Removing account is implemented")
+ void removeAccount() throws Exception {
+ Account account = create("Johnny", "Boy", "jboy@gmail.com");
+ accountDao.save(account);
+
+ mockMvc.perform(delete(String.format("/accounts/%d", account.getId())))
+ .andExpect(status().isNoContent());
+ }
+}
+
diff --git a/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/WebAppConfigurationTest.java b/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/WebAppConfigurationTest.java
new file mode 100644
index 0000000..6b5640d
--- /dev/null
+++ b/3-0-spring-framework/3-2-1-account-rest-api/src/test/java/com/bobocode/WebAppConfigurationTest.java
@@ -0,0 +1,76 @@
+package com.bobocode;
+
+import com.bobocode.config.RootConfig;
+import com.bobocode.config.WebConfig;
+import com.bobocode.dao.impl.InMemoryAccountDao;
+import org.junit.jupiter.api.*;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class WebAppConfigurationTest {
+
+ @Test
+ @Order(1)
+ @DisplayName("RootConfig class is configured properly")
+ void rootConfigClassIsConfigured() {
+ Configuration configuration = RootConfig.class.getAnnotation(Configuration.class);
+ ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class);
+ String[] packages = componentScan.basePackages();
+ if (packages.length == 0) {
+ packages = componentScan.value();
+ }
+
+ assertNotNull(configuration);
+ assertNotNull(componentScan);
+ assertThat(packages).contains("com.bobocode");
+
+ Filter[] filters = componentScan.excludeFilters();
+ List