diff --git a/ContactsAPI.postman_collection.json b/ContactsAPI.postman_collection.json index 9e7f8b1..178d9d0 100644 --- a/ContactsAPI.postman_collection.json +++ b/ContactsAPI.postman_collection.json @@ -71,7 +71,7 @@ } }, "url": { - "raw": "localhost:8080/api/v1/contacts/1", + "raw": "localhost:8080/api/v1/contacts/5", "host": [ "localhost" ], @@ -80,9 +80,10 @@ "api", "v1", "contacts", - "1" + "5" ] - } + }, + "description": "Modify the ID, in this case '5', at the end of the URL to the desired contact ID and modify fields in the message body as needed." }, "response": [] }, @@ -92,7 +93,7 @@ "method": "DELETE", "header": [], "url": { - "raw": "localhost:8080/api/v1/contacts/1", + "raw": "localhost:8080/api/v1/contacts/5", "host": [ "localhost" ], @@ -101,9 +102,10 @@ "api", "v1", "contacts", - "1" + "5" ] - } + }, + "description": "Modify the ID, in this case '5', at the end of the URL to the desired contact ID as needed." }, "response": [] }, diff --git a/README.md b/README.md index 76b37b4..a650bd6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -# contacts-api +# Contacts REST API +## About +The aim of this exercise is to create a simple contact entry system. It consists of a REST API that will enable a client to perform +CRUD operations on the contact collection. +## Requirements +The API must have the following endpoints: + +| Method | Endpoint | Description | +| ------ | -------- | ----------- | +| **GET** | **/contacts** | List all contacts | +| **POST** | **/contacts** | Create a new contact | +| **PUT** | **/contacts/{id}** | Update a contact | +| **GET** | **/contacts/{id}** | Get a specific contact | +| **DELETE** | **/contacts/{id}** | Delete a contact | +| **GET** | **/contacts/call-list** | List of all contacts ^1^ | + +^1^ Restricted to contacts with a home phone sorted by last name then first name + +## Software +The following software is required in order to build and run: + +1. [**Maven**](https://maven.apache.org/download.cgi) +2. [**Java JDK 8**](https://openjdk.java.net/install/) or later + +## Implementation +I chose to use the [**Spring Framework**](https://spring.io/) and more specifically, the [**Spring Boot**](https://spring.io/projects/spring-boot) and [**Spring Data JPA**](https://spring.io/projects/spring-data-jpa) projects as the base for this API. +**Spring Boot** makes it simple to create a stand-alone REST API and **Spring Data JPA** simplifies interfacing with an h2 database. + +## Build and Run +To build and run, from the command line, navigate to the root directory and run the command: +```sh +mvn spring-boot:run +``` +The URL to access the API is: [**http://localhost:8080/api/v1/contacts**](http://localhost:8080/api/v1/contacts) + +## Running Tests +To run tests, from the command line, navigate to the root directory and run the command: +```sh +mvn test +``` + +## Additional Resources +To prepopulate the database with demo data, uncomment the following lines in the 'src/main/resources/application.properties' file +```sh +# spring.jpa.defer-datasource-initialization=true +# spring.sql.init.data-locations=classpath:demo.sql +``` + +An H2 console will be available at [**http://localhost:8080/h2-console**](http://localhost:8080/h2-console) to allow direct access to the underlying H2 database + +username: sa /password: leave empty + +Also included is the file 'ContactsAPI.postman_collection.json'. This can be imported into the [**Postman**](https://www.postman.com/downloads/) application in order to issue REST commands. diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6279664..c0c7f9f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,18 +1,24 @@ # ---Comments without preceeding '---' are settings to enable for development + # ---Enable H2 console spring.h2.console.enabled=true + # ---Turn Statistics on spring.jpa.properties.hibernate.generate_statistics=true logging.level.org.hibernate.stat=debug logging.level.org.hibernate.type=debug + # ---Show queries # spring.jpa.show-sql=true # spring.jpa.properties.hibernate.format_sql=true + # ---In memory DB spring.datasource.url=jdbc:h2:mem:contactsdb spring.data.jpa.repositories.bootstrap-mode=default + # ---Uncomment the next properties to load demo data from file # spring.jpa.defer-datasource-initialization=true # spring.sql.init.data-locations=classpath:demo.sql + # ---For development only # server.error.include-message=always diff --git a/src/test/java/com/singlestone/contacts/controller/ContactControllerTest.java b/src/test/java/com/singlestone/contacts/controller/ContactControllerTest.java index 0e41777..8e6533b 100644 --- a/src/test/java/com/singlestone/contacts/controller/ContactControllerTest.java +++ b/src/test/java/com/singlestone/contacts/controller/ContactControllerTest.java @@ -69,6 +69,14 @@ public class ContactControllerTest { return testContactDTO; } + private static String asJsonString(final Object obj){ + try{ + return new ObjectMapper().writeValueAsString(obj); + }catch (Exception e){ + throw new RuntimeException(e); + } + } + @BeforeEach public void setup(){ mockMvc = MockMvcBuilders.standaloneSetup(contactController).build(); @@ -79,9 +87,9 @@ public class ContactControllerTest { ContactDTO contactToCreate = createTestContactDTO(); when(contactService.createContact(any())).thenReturn(contactToCreate); mockMvc.perform(post("/api/v1/contacts"). - contentType(MediaType.APPLICATION_JSON). - content(asJsonString(contactToCreate))). - andExpect(status().isCreated()); + contentType(MediaType.APPLICATION_JSON). + content(asJsonString(contactToCreate))). + andExpect(status().isCreated()); verify(contactService, times(1)).createContact(any()); } @@ -92,9 +100,9 @@ public class ContactControllerTest { contacts.add(contactToCreate); when(contactService.getAllContacts()).thenReturn(contacts); mockMvc.perform(get("/api/v1/contacts"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isOk()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isOk()); verify(contactService, times(1)).getAllContacts(); } @@ -103,9 +111,9 @@ public class ContactControllerTest { List contacts = new ArrayList<>(); when(contactService.getAllContacts()).thenReturn(contacts); mockMvc.perform(get("/api/v1/contacts"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isNoContent()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isNoContent()); verify(contactService, times(1)).getAllContacts(); } @@ -114,9 +122,9 @@ public class ContactControllerTest { ContactDTO contactToGet = createTestContactDTO(); when(contactService.getContact(anyLong())).thenReturn(contactToGet); mockMvc.perform(get("/api/v1/contacts/1"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isOk()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isOk()); verify(contactService, times(1)).getContact(anyLong()); } @@ -124,9 +132,9 @@ public class ContactControllerTest { void testGetContactDoesNotExist() throws Exception { when(contactService.getContact(anyLong())).thenReturn(null); mockMvc.perform(get("/api/v1/contacts/1"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isNotFound()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isNotFound()); verify(contactService, times(1)).getContact(anyLong()); } @@ -135,9 +143,9 @@ public class ContactControllerTest { ContactDTO contactToUpdate = createTestContactDTO(); when(contactService.updateContact(anyLong(), any())).thenReturn(contactToUpdate); mockMvc.perform(put("/api/v1/contacts/1"). - contentType(MediaType.APPLICATION_JSON). - content(asJsonString(contactToUpdate))). - andExpect(status().isOk()); + contentType(MediaType.APPLICATION_JSON). + content(asJsonString(contactToUpdate))). + andExpect(status().isOk()); verify(contactService, times(1)).updateContact(anyLong(), any()); } @@ -146,9 +154,9 @@ public class ContactControllerTest { ContactDTO contact = createTestContactDTO(); when(contactService.updateContact(anyLong(), any())).thenReturn(null); mockMvc.perform(put("/api/v1/contacts/1"). - contentType(MediaType.APPLICATION_JSON). - content(asJsonString(contact))). - andExpect(status().isNotFound()); + contentType(MediaType.APPLICATION_JSON). + content(asJsonString(contact))). + andExpect(status().isNotFound()); verify(contactService, times(1)).updateContact(anyLong(), any()); } @@ -157,9 +165,9 @@ public class ContactControllerTest { ContactDTO contactToDelete = createTestContactDTO(); when(contactService.deleteContact(anyLong())).thenReturn(contactToDelete); mockMvc.perform(delete("/api/v1/contacts/1"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isNoContent()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isNoContent()); verify(contactService, times(1)).deleteContact(anyLong()); } @@ -167,9 +175,9 @@ public class ContactControllerTest { void testDeleteContactDoesNotExist() throws Exception { when(contactService.deleteContact(anyLong())).thenReturn(null); mockMvc.perform(delete("/api/v1/contacts/1"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isNotFound()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isNotFound()); verify(contactService, times(1)).deleteContact(anyLong()); } @@ -182,9 +190,9 @@ public class ContactControllerTest { contacts.add(contactToreturn); when(contactService.getCallList()).thenReturn(contacts); mockMvc.perform(get("/api/v1/contacts/call-list"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isOk()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isOk()); verify(contactService, times(1)).getCallList(); } @@ -193,17 +201,9 @@ public class ContactControllerTest { List contacts = new ArrayList<>(); when(contactService.getCallList()).thenReturn(contacts); mockMvc.perform(get("/api/v1/contacts/call-list"). - accept(MediaType.APPLICATION_JSON). - content("")). - andExpect(status().isNoContent()); + accept(MediaType.APPLICATION_JSON). + content("")). + andExpect(status().isNoContent()); verify(contactService, times(1)).getCallList(); } - - public static String asJsonString(final Object obj){ - try{ - return new ObjectMapper().writeValueAsString(obj); - }catch (Exception e){ - throw new RuntimeException(e); - } - } } diff --git a/src/test/java/com/singlestone/contacts/service/ContactServiceTest.java b/src/test/java/com/singlestone/contacts/service/ContactServiceTest.java index 307e119..3c0b41b 100644 --- a/src/test/java/com/singlestone/contacts/service/ContactServiceTest.java +++ b/src/test/java/com/singlestone/contacts/service/ContactServiceTest.java @@ -100,6 +100,7 @@ public class ContactServiceTest { void testDeleteContact() { when(contactRepository.findById(anyLong())).thenReturn(Optional.of(createTestContact())); contactService.deleteContact(1); + verify(contactRepository, times(1)).findById(anyLong()); verify(contactRepository, times(1)).deleteById(anyLong()); }