diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e48fb8a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +name: Java Practice CI build + +on: + push: + branches: [ "main "] + pull_request: + branches: [ "main" ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Build and Test with Maven + run: mvn -B verify \ No newline at end of file diff --git a/src/main/java/com/practice/apipractice/client/JsonPlaceHolderClient.java b/src/main/java/com/practice/apipractice/client/JsonPlaceHolderClient.java index 5a94a38..f2efcf2 100644 --- a/src/main/java/com/practice/apipractice/client/JsonPlaceHolderClient.java +++ b/src/main/java/com/practice/apipractice/client/JsonPlaceHolderClient.java @@ -1,5 +1,6 @@ package com.practice.apipractice.client; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.practice.apipractice.model.Post; @@ -24,7 +25,6 @@ public JsonPlaceHolderClient() { @Override public Optional getPostById(int id) { - // TODO: Implement HTTP request and JSON parsing URI uri = URI.create(BASE_URL + "/posts/" + id); HttpRequest request = HttpRequest.newBuilder() .uri(uri) @@ -48,4 +48,30 @@ public Optional getPostById(int id) { return Optional.empty(); } } + + @Override + public Optional createPost(Post postToCreate) { + try { + String jsonPost = objectMapper.writeValueAsString(postToCreate); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/posts/")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPost)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 201) { + Post post = objectMapper.readValue(response.body(), Post.class); + return Optional.of(post); + } else { + System.err.println("Request failed with status code: " + response.statusCode() + " and body: " + response.body()); + return Optional.empty(); + } + } catch (IOException | InterruptedException e ) { + System.err.println("Error creating Post: " + e.getMessage()); + e.printStackTrace(); + return Optional.empty(); + } + } } diff --git a/src/main/java/com/practice/apipractice/client/PostApiClient.java b/src/main/java/com/practice/apipractice/client/PostApiClient.java index cfb884a..096ce09 100644 --- a/src/main/java/com/practice/apipractice/client/PostApiClient.java +++ b/src/main/java/com/practice/apipractice/client/PostApiClient.java @@ -6,4 +6,6 @@ public interface PostApiClient { Optional getPostById(int id); + + Optional createPost(Post postToCreate); } diff --git a/src/main/java/com/practice/apipractice/service/PostService.java b/src/main/java/com/practice/apipractice/service/PostService.java new file mode 100644 index 0000000..643d392 --- /dev/null +++ b/src/main/java/com/practice/apipractice/service/PostService.java @@ -0,0 +1,9 @@ +package com.practice.apipractice.service; + +import com.practice.apipractice.model.Post; + +import java.util.Optional; + +public interface PostService { + Optional createPost(Post postToCreate); +} diff --git a/src/main/java/com/practice/apipractice/service/PostServiceImpl.java b/src/main/java/com/practice/apipractice/service/PostServiceImpl.java new file mode 100644 index 0000000..37d741a --- /dev/null +++ b/src/main/java/com/practice/apipractice/service/PostServiceImpl.java @@ -0,0 +1,25 @@ +package com.practice.apipractice.service; + +import com.practice.apipractice.client.PostApiClient; +import com.practice.apipractice.model.Post; + +import java.util.Optional; + +public class PostServiceImpl implements PostService { + + private final PostApiClient postApiClient; + + public PostServiceImpl(PostApiClient postApiClient) { + this.postApiClient = postApiClient; + } + + @Override + public Optional createPost(Post postToCreate) { + String title = postToCreate.title(); + + if ( title == null || title.isBlank()) { + return Optional.empty(); + } + return postApiClient.createPost(postToCreate); + }; +} diff --git a/src/test/java/com/practice/apipractice/client/PostApiTests.java b/src/test/java/com/practice/apipractice/client/PostApiTests.java new file mode 100644 index 0000000..81989ec --- /dev/null +++ b/src/test/java/com/practice/apipractice/client/PostApiTests.java @@ -0,0 +1,70 @@ +package com.practice.apipractice.client; + +import com.practice.apipractice.model.Post; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PostApiTests { + + private PostApiClient postApiClient; + + @BeforeEach + void setup() { + postApiClient = new JsonPlaceHolderClient(); + } + + @Test + void getPostById_WithValidId_ReturnsPost () { + // GIVEN a Post Id that is known to exist + int postId = 1; + + // WHEN the client is called with the Post Id + Optional result = postApiClient.getPostById(postId); + + // THEN the optional should be present + assertThat(result).isPresent(); + + // AND the Post object fields should be correct + Post post = result.get(); + assertThat(post.id()).isEqualTo(postId); + assertThat(post.userId()).isNotNull().isPositive(); + assertThat(post.title()).isNotBlank(); + assertThat(post.body()).isNotBlank(); + } + + @Test + void getPostById_WithInvalidId_ReturnsEmpty() { + // GIVEN a Post Id that is known to be invalid + int invalidId = -99; + + // WHEN the client is called with the Post Id + Optional result = postApiClient.getPostById(invalidId); + + // THEN the API should return 404 which the client should return as an empty Optional + assertThat(result).isEmpty(); + } + + @Test + void createPost_withValidPost_Returns201AndPost() { + // GIVEN a valid post to create + Post postToCreate = new Post(1, null, "Test Title", "Test Body"); + + // WHEN the client is called with the post as request body + Optional result = postApiClient.createPost(postToCreate); + + // THEN the post creation should succeed + assertThat(result).isPresent(); + + // AND the Post object fields should be appropriate + Post postResponse = result.get(); + assertThat(postResponse.id()).isNotNull(); + assertThat(postResponse.id()).isPositive(); + assertThat(postResponse.userId()).isEqualTo(postToCreate.userId()); + assertThat(postResponse.title()).isEqualTo(postToCreate.title()); + assertThat(postResponse.body()).isEqualTo(postToCreate.body()); + } +} diff --git a/src/test/java/com/practice/apipractice/service/PostServiceUnitTests.java b/src/test/java/com/practice/apipractice/service/PostServiceUnitTests.java new file mode 100644 index 0000000..ec6f27c --- /dev/null +++ b/src/test/java/com/practice/apipractice/service/PostServiceUnitTests.java @@ -0,0 +1,84 @@ +package com.practice.apipractice.service; + +import com.practice.apipractice.client.PostApiClient; +import com.practice.apipractice.model.Post; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class PostServiceUnitTests { + + @Mock + private PostApiClient mockPostApiClient; + + @InjectMocks + private PostServiceImpl postService; + + @Test + void createPost_WhenClientSucceeds_ReturnsCreatedPost() { + // GIVEN a valid post to create with no Id field + Post postToCreate = new Post(1, null, "Test Title", "Test Body"); + + // AND the expected post returned from the API (now with Id) + Post expectedPostFromApi = new Post(1, 101, "Test Title", "Test Body"); + + // AND a mock client configured to return the expected Post when called appropriately + when(mockPostApiClient.createPost(postToCreate)).thenReturn(Optional.of(expectedPostFromApi)); + + // WHEN the service method is called + Optional result = postService.createPost(postToCreate); + + // THEN the service should call the client once + verify(mockPostApiClient).createPost(postToCreate); + + // AND the Post should be returned correctly + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(expectedPostFromApi); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", " ", "\t", "\n"}) + void createPost_withInvalidTitle_ReturnsEmpty_AndDoesNotCallClient(String invalidTitle) { + // GIVEN a post with an invalid title (which is now passed in as a parameter) + Post postWithInvalidTitle = new Post(1, null, invalidTitle, "Test Body"); + + // WHEN we call the service with this invalid post + Optional result = postService.createPost(postWithInvalidTitle); + + // THEN the result should be an empty Optional + assertThat(result).isEmpty(); + + // AND the client's createPost method should NEVER have been called. + verify(mockPostApiClient, never()).createPost(any(Post.class)); + } + + @Test + void createPost_whenClientFails_ReturnsEmpty () { + // GIVEN a valid post to create + Post postToCreate = new Post(1, null, "Test Title", "Test Body"); + + // AND a mock client configured to fail and return Empty when called + when(mockPostApiClient.createPost(postToCreate)).thenReturn(Optional.empty()); + + // WHEN the service method is called + Optional result = postService.createPost(postToCreate); + + // THEN the service should call the client once + verify(mockPostApiClient).createPost(postToCreate); + + // AND the result should be an empty Optional + assertThat(result).isEmpty(); + } +}