diff --git a/.gitignore b/.gitignore index c9d7b468..92113738 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,6 @@ buildNumber.properties .settings/ mvnw -mvnw.cmd \ No newline at end of file +mvnw.cmd + +.idea diff --git a/README.md b/README.md index 538051ae..b884b82b 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ To keep track of job applications: 1. Use Google Spreadsheet (I am using it before this project). It is ugly, hard to maintain and easy to mess up the layout. 1. Use online tool like huntr.co. But it can only track 40 jobs and I don't want to spend a dime for the premium service. -1. Use pen and paper. It works but I can't copy and paste information such as link job link and utilize it later. +1. Use pen and paper. It works but I can't copy and paste information such as the job url link and utilize it later. None of the about is fun or totally fit my use case. @@ -19,7 +19,7 @@ That's the reason for me to build something for myself, and potentially you, to With Job Winner, you can keep track of how many job applications you want, for free, without paying for a fee and have your personal data being sold. -Another feature of Job Winner is the profile page. With that one can store useful information a job application usually asks for (eg. LinkedIn url) in a single section. Besides that, the profile page provides a handy feature to automatically copy the field that you click on to your clipboard. +Another feature of Job Winner is the `Profile Page`. With that one can store useful information a job application usually asks for (eg. LinkedIn url) in a single section. Besides that, the `Profile Page` provides a handy feature to automatically copy the field that you click on to your clipboard. ## Features diff --git a/src/main/java/com/tnite/jobwinner/controller/ProfileController.java b/src/main/java/com/tnite/jobwinner/controller/ProfileController.java index 33869ff5..bf0ec898 100644 --- a/src/main/java/com/tnite/jobwinner/controller/ProfileController.java +++ b/src/main/java/com/tnite/jobwinner/controller/ProfileController.java @@ -21,8 +21,7 @@ public class ProfileController { @MutationMapping public Mono addProfile(@Argument AddProfileInput addProfileInput) { - Mono profile = profileService.addProfile(addProfileInput); - return profile; + return profileService.addProfile(addProfileInput); } @MutationMapping @@ -32,8 +31,12 @@ public Mono updateProfile(@Argument Profile profile) { @QueryMapping public Flux allProfile() { - Flux profile = profileService.allProfile(); - return profile; + return profileService.allProfile(); + } + + @QueryMapping + public Mono getProfile(@Argument Integer id) { + return profileService.getProfile(id); } } \ No newline at end of file diff --git a/src/main/java/com/tnite/jobwinner/service/ProfileService.java b/src/main/java/com/tnite/jobwinner/service/ProfileService.java index 0b9fd053..1f7ad5b3 100644 --- a/src/main/java/com/tnite/jobwinner/service/ProfileService.java +++ b/src/main/java/com/tnite/jobwinner/service/ProfileService.java @@ -7,7 +7,6 @@ import com.tnite.jobwinner.model.Profile; import com.tnite.jobwinner.repo.ProfileRepository; -import graphql.com.google.common.base.Function; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,71 +14,69 @@ @Service @Slf4j public class ProfileService { - + @Autowired private ProfileRepository profileRepository; - - Function mapping = p -> { - var profile = new Profile(); - profile.setFirstName(p.getFirstName()); - profile.setLastName(p.getLastName()); - profile.setAddressStreet1(p.getAddressStreet1()); - profile.setAddressStreet2(p.getAddressStreet2()); - profile.setAddressCity(p.getAddressCity()); - profile.setAddressState(p.getAddressState()); - profile.setAddressZip(p.getAddressZip()); - profile.setLinkedin(p.getLinkedin()); - profile.setGithub(p.getGithub()); - profile.setPersonalWebsite(p.getPersonalWebsite()); - return profile; - }; - - Function editMapping = p -> { + + private Profile mapToProfile(AddProfileInput addProfileInput) { var profile = new Profile(); - profile.setId(p.getId()); - profile.setFirstName(p.getFirstName()); - profile.setLastName(p.getLastName()); - profile.setAddressStreet1(p.getAddressStreet1()); - profile.setAddressStreet2(p.getAddressStreet2()); - profile.setAddressCity(p.getAddressCity()); - profile.setAddressState(p.getAddressState()); - profile.setAddressZip(p.getAddressZip()); - profile.setLinkedin(p.getLinkedin()); - profile.setGithub(p.getGithub()); - profile.setPersonalWebsite(p.getPersonalWebsite()); + profile.setFirstName(addProfileInput.getFirstName()); + profile.setLastName(addProfileInput.getLastName()); + profile.setAddressStreet1(addProfileInput.getAddressStreet1()); + profile.setAddressStreet2(addProfileInput.getAddressStreet2()); + profile.setAddressCity(addProfileInput.getAddressCity()); + profile.setAddressState(addProfileInput.getAddressState()); + profile.setAddressZip(addProfileInput.getAddressZip()); + profile.setLinkedin(addProfileInput.getLinkedin()); + profile.setGithub(addProfileInput.getGithub()); + profile.setPersonalWebsite(addProfileInput.getPersonalWebsite()); return profile; - }; - - + } + public Mono addProfile(AddProfileInput addProfileInput) { - Mono profile = profileRepository.save(mapping.apply(addProfileInput)); - log.info("Added new profile: {}", addProfileInput); - return profile; + Profile profile = mapToProfile(addProfileInput); + return profileRepository.save(profile) + .doOnSuccess(p -> log.info("Added new profile: {}", p)) + .doOnError(e -> log.error("Failed to add profile: {}", addProfileInput, e)); } - public Mono updateProfile(Profile profile) { - log.info("Updating profile id {}, {}", profile.getId()); - return this.profileRepository.findById(profile.getId()) - .flatMap(p -> { - p.setFirstName(profile.getFirstName()); - p.setLastName(profile.getLastName()); - p.setAddressStreet1(profile.getAddressStreet1()); - p.setAddressStreet2(profile.getAddressStreet2()); - p.setAddressCity(profile.getAddressCity()); - p.setAddressState(profile.getAddressState()); - p.setAddressZip(profile.getAddressZip()); - p.setLinkedin(profile.getLinkedin()); - p.setGithub(profile.getGithub()); - p.setPersonalWebsite(profile.getPersonalWebsite()); - return profileRepository.save(profile).log(); - }); + return profileRepository.findById(profile.getId()) + .flatMap(existingProfile -> { + updateProfileDetails(existingProfile, profile); + return profileRepository.save(existingProfile); + }) + .doOnSuccess(p -> log.info("Updated profile: {}", p)) + .doOnError(e -> log.error("Failed to update profile: {}", profile, e)); + } + + private void updateProfileDetails(Profile existingProfile, Profile updatedProfile) { + existingProfile.setFirstName(updatedProfile.getFirstName()); + existingProfile.setLastName(updatedProfile.getLastName()); + existingProfile.setAddressStreet1(updatedProfile.getAddressStreet1()); + existingProfile.setAddressStreet2(updatedProfile.getAddressStreet2()); + existingProfile.setAddressCity(updatedProfile.getAddressCity()); + existingProfile.setAddressState(updatedProfile.getAddressState()); + existingProfile.setAddressZip(updatedProfile.getAddressZip()); + existingProfile.setLinkedin(updatedProfile.getLinkedin()); + existingProfile.setGithub(updatedProfile.getGithub()); + existingProfile.setPersonalWebsite(updatedProfile.getPersonalWebsite()); } - public Flux allProfile() { - return this.profileRepository.findAll().log(); + return profileRepository.findAll() + .doOnComplete(() -> log.info("Retrieved all profiles")) + .doOnError(e -> log.error("Failed to retrieve profiles", e)); } + public Mono getProfile(Integer id) { + return profileRepository.findById(id) + .switchIfEmpty(Mono.defer(() -> { + log.warn("Profile with id {} not found", id); + return Mono.empty(); + })) + .doOnSuccess(profile -> log.info("Retrieved profile: {}", profile)) + .doOnError(e -> log.error("Failed to retrieve profile with id {}", id, e)); + } } diff --git a/src/test/java/com/tnite/jobwinner/controller/ProfileControllerTest.java b/src/test/java/com/tnite/jobwinner/controller/ProfileControllerTest.java index bc9ddcac..1d1ed1bc 100644 --- a/src/test/java/com/tnite/jobwinner/controller/ProfileControllerTest.java +++ b/src/test/java/com/tnite/jobwinner/controller/ProfileControllerTest.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -66,4 +67,14 @@ void testAllProfileWhenNoProfilesThenReturnEmptyResult() { assertEquals(0, result.collectList().block().size()); } + + @Test + void getProfile() { + Profile profile1 = new Profile(); + when(profileService.getProfile(anyInt())).thenReturn(Mono.just(profile1)); + + Mono result = profileController.getProfile(1); + + assertEquals(profile1, result.block()); + } } diff --git a/src/test/java/com/tnite/jobwinner/service/ProfileServiceTest.java b/src/test/java/com/tnite/jobwinner/service/ProfileServiceTest.java new file mode 100644 index 00000000..7dbe6d1c --- /dev/null +++ b/src/test/java/com/tnite/jobwinner/service/ProfileServiceTest.java @@ -0,0 +1,196 @@ +package com.tnite.jobwinner.service; + +import com.tnite.jobwinner.model.AddProfileInput; +import com.tnite.jobwinner.model.Profile; +import com.tnite.jobwinner.repo.ProfileRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ProfileServiceTest { + + @Mock + private ProfileRepository profileRepository; + + @InjectMocks + private ProfileService profileService; + + private Profile profile1; + private Profile profile2; + private AddProfileInput addProfileInput1; + private AddProfileInput addProfileInput2; + private List profileList; + + @BeforeEach + void setUp() { + profile1 = new Profile(); + profile1.setId(1); + profile1.setFirstName("John"); + profile1.setLastName("Doe"); + profile1.setAddressStreet1("123 Main St"); + profile1.setAddressStreet2("Apt 4"); + profile1.setAddressCity("City"); + profile1.setAddressState("State"); + profile1.setAddressZip("12345"); + profile1.setLinkedin("john.doe"); + profile1.setGithub("johndoe"); + profile1.setPersonalWebsite("www.johndoe.com"); + + profile2 = new Profile(); + profile2.setId(2); + profile2.setFirstName("Jane"); + profile2.setLastName("Smith"); + profile2.setAddressStreet1("456 Elm St"); + profile2.setAddressStreet2("Suite 5"); + profile2.setAddressCity("Town"); + profile2.setAddressState("Province"); + profile2.setAddressZip("67890"); + profile2.setLinkedin("jane.smith"); + profile2.setGithub("janesmith"); + profile2.setPersonalWebsite("www.janesmith.com"); + + addProfileInput1 = new AddProfileInput(); + addProfileInput1.setFirstName("John"); + addProfileInput1.setLastName("Doe"); + addProfileInput1.setAddressStreet1("123 Main St"); + addProfileInput1.setAddressStreet2("Apt 4"); + addProfileInput1.setAddressCity("City"); + addProfileInput1.setAddressState("State"); + addProfileInput1.setAddressZip("12345"); + addProfileInput1.setLinkedin("john.doe"); + addProfileInput1.setGithub("johndoe"); + addProfileInput1.setPersonalWebsite("www.johndoe.com"); + + addProfileInput2 = new AddProfileInput(); + addProfileInput2.setFirstName("Jane"); + addProfileInput2.setLastName("Smith"); + addProfileInput2.setAddressStreet1("456 Elm St"); + addProfileInput2.setAddressStreet2("Suite 5"); + addProfileInput2.setAddressCity("Town"); + addProfileInput2.setAddressState("Province"); + addProfileInput2.setAddressZip("67890"); + addProfileInput2.setLinkedin("jane.smith"); + addProfileInput2.setGithub("janesmith"); + addProfileInput2.setPersonalWebsite("www.janesmith.com"); + + profileList = Arrays.asList(profile1, profile2); + } + + @Test + void testAddProfile() { + when(profileRepository.save(ArgumentMatchers.any(Profile.class))).thenReturn(Mono.just(profile1)); + + Mono result = profileService.addProfile(addProfileInput1); + + StepVerifier.create(result) + .assertNext(savedProfile -> { + assertEquals(profile1.getId(), savedProfile.getId()); + assertEquals(profile1.getFirstName(), savedProfile.getFirstName()); + assertEquals(profile1.getLastName(), savedProfile.getLastName()); + assertEquals(profile1.getAddressStreet1(), savedProfile.getAddressStreet1()); + assertEquals(profile1.getAddressStreet2(), savedProfile.getAddressStreet2()); + assertEquals(profile1.getAddressCity(), savedProfile.getAddressCity()); + assertEquals(profile1.getAddressState(), savedProfile.getAddressState()); + assertEquals(profile1.getAddressZip(), savedProfile.getAddressZip()); + assertEquals(profile1.getLinkedin(), savedProfile.getLinkedin()); + assertEquals(profile1.getGithub(), savedProfile.getGithub()); + assertEquals(profile1.getPersonalWebsite(), savedProfile.getPersonalWebsite()); + }) + .verifyComplete(); + + verify(profileRepository, times(1)).save(any(Profile.class)); + } + + @Test + void testUpdateProfile() { + when(profileRepository.findById(1)).thenReturn(Mono.just(profile1)); + when(profileRepository.save(any(Profile.class))).thenReturn(Mono.just(profile1)); + + Mono result = profileService.updateProfile(profile1); + + StepVerifier.create(result) + .assertNext(updatedProfile -> { + assertEquals(profile1.getId(), updatedProfile.getId()); + assertEquals(profile1.getFirstName(), updatedProfile.getFirstName()); + assertEquals(profile1.getLastName(), updatedProfile.getLastName()); + assertEquals(profile1.getAddressStreet1(), updatedProfile.getAddressStreet1()); + assertEquals(profile1.getAddressStreet2(), updatedProfile.getAddressStreet2()); + assertEquals(profile1.getAddressCity(), updatedProfile.getAddressCity()); + assertEquals(profile1.getAddressState(), updatedProfile.getAddressState()); + assertEquals(profile1.getAddressZip(), updatedProfile.getAddressZip()); + assertEquals(profile1.getLinkedin(), updatedProfile.getLinkedin()); + assertEquals(profile1.getGithub(), updatedProfile.getGithub()); + assertEquals(profile1.getPersonalWebsite(), updatedProfile.getPersonalWebsite()); + }) + .verifyComplete(); + + verify(profileRepository, times(1)).findById(1); + verify(profileRepository, times(1)).save(any(Profile.class)); + } + + @Test + void testAllProfile() { + when(profileRepository.findAll()).thenReturn(Flux.fromIterable(profileList)); + + Flux result = profileService.allProfile(); + + StepVerifier.create(result) + .expectNext(profile1) + .expectNext(profile2) + .verifyComplete(); + + verify(profileRepository, times(1)).findAll(); + } + + @Test + void testGetProfile() { + when(profileRepository.findById(1)).thenReturn(Mono.just(profile1)); + + Mono result = profileService.getProfile(1); + + StepVerifier.create(result) + .assertNext(retrievedProfile -> { + assertEquals(profile1.getId(), retrievedProfile.getId()); + assertEquals(profile1.getFirstName(), retrievedProfile.getFirstName()); + assertEquals(profile1.getLastName(), retrievedProfile.getLastName()); + assertEquals(profile1.getAddressStreet1(), retrievedProfile.getAddressStreet1()); + assertEquals(profile1.getAddressStreet2(), retrievedProfile.getAddressStreet2()); + assertEquals(profile1.getAddressCity(), retrievedProfile.getAddressCity()); + assertEquals(profile1.getAddressState(), retrievedProfile.getAddressState()); + assertEquals(profile1.getAddressZip(), retrievedProfile.getAddressZip()); + assertEquals(profile1.getLinkedin(), retrievedProfile.getLinkedin()); + assertEquals(profile1.getGithub(), retrievedProfile.getGithub()); + assertEquals(profile1.getPersonalWebsite(), retrievedProfile.getPersonalWebsite()); + }) + .verifyComplete(); + + verify(profileRepository, times(1)).findById(1); + } + + @Test + void testGetProfileWhenProfileNotFound() { + when(profileRepository.findById(3)).thenReturn(Mono.empty()); + + Mono result = profileService.getProfile(3); + + StepVerifier.create(result) + .expectNextCount(0) + .verifyComplete(); + + verify(profileRepository, times(1)).findById(3); + } +}