Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -24,7 +25,6 @@ public JsonPlaceHolderClient() {

@Override
public Optional<Post> getPostById(int id) {
// TODO: Implement HTTP request and JSON parsing
URI uri = URI.create(BASE_URL + "/posts/" + id);
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
Expand All @@ -48,4 +48,30 @@ public Optional<Post> getPostById(int id) {
return Optional.empty();
}
}

@Override
public Optional<Post> 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<String> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

public interface PostApiClient {
Optional<Post> getPostById(int id);

Optional<Post> createPost(Post postToCreate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.practice.apipractice.service;

import com.practice.apipractice.model.Post;

import java.util.Optional;

public interface PostService {
Optional<Post> createPost(Post postToCreate);
}
Original file line number Diff line number Diff line change
@@ -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<Post> createPost(Post postToCreate) {
String title = postToCreate.title();

if ( title == null || title.isBlank()) {
return Optional.empty();
}
return postApiClient.createPost(postToCreate);
};
}
70 changes: 70 additions & 0 deletions src/test/java/com/practice/apipractice/client/PostApiTests.java
Original file line number Diff line number Diff line change
@@ -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<Post> 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<Post> 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<Post> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Post> 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<Post> 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<Post> 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();
}
}
Loading