diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a4b4bac..c402990 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,13 +22,15 @@ jobs: ref: main fetch-depth: 0 - - name: Save fly.toml from deploy branch + - name: Save files from deploy branch run: | git fetch origin deploy || echo "No deploy branch yet" mkdir temp if git ls-remote --exit-code origin deploy; then - git checkout origin/deploy -- fly.toml || echo "No fly.toml to copy" - cp fly.toml temp/ || echo "No fly.toml found to copy" + for file in fly.toml .dockerignore; do + git checkout origin/deploy -- "$file" 2>/dev/null || echo "No $file to copy" + [ -f "$file" ] && cp "$file" temp/ || echo "$file not found" + done fi - name: Create/overwrite deploy branch @@ -36,16 +38,18 @@ jobs: git checkout -B deploy git reset --hard main - - name: Restore fly.toml + - name: Restore files run: | - if [ -f temp/fly.toml ]; then - cp temp/fly.toml fly.toml - fi + for file in fly.toml .dockerignore; do + if [ -f temp/$file ]; then + cp temp/$file $file + fi + done - name: Commit and push to deploy run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A - git commit -m "Sync main -> deploy (keeping fly.toml)" || echo "No changes to commit" - git push -f origin deploy + git commit -m "Sync main -> deploy (keeping fly.toml and .dockerignore)" || echo "No changes to commit" + git push -f origin deploy \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2240980 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Run Unit Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Run unit tests + run: mvn test \ No newline at end of file diff --git a/pom.xml b/pom.xml index 401a7ce..d1ecfb8 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,12 @@ jackson-databind 2.15.2 + + org.mockito + mockito-core + 5.20.0 + test + diff --git a/src/test/java/br/com/notehub/implementation/user/UserCreationTest.java b/src/test/java/br/com/notehub/implementation/user/UserCreationTest.java new file mode 100644 index 0000000..9ddb517 --- /dev/null +++ b/src/test/java/br/com/notehub/implementation/user/UserCreationTest.java @@ -0,0 +1,156 @@ +package br.com.notehub.implementation.user; + +import br.com.notehub.application.implementation.user.UserServiceImpl; +import br.com.notehub.domain.history.UserHistoryService; +import br.com.notehub.domain.token.TokenService; +import br.com.notehub.domain.user.User; +import br.com.notehub.domain.user.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserCreationTest { + + @InjectMocks + private UserServiceImpl service; + + @Mock + private UserRepository repository; + + @Mock + private TokenService tokenService; + + @Mock + private UserHistoryService historian; + + @Mock + private PasswordEncoder encoder; + + private User user; + + private User createUser(String email, String username) { + User u = new User(email, username, username.toUpperCase(), "123"); + u.setId(UUID.randomUUID()); + u.setActive(true); + return u; + } + + private void mockExistsByEmail(User u, boolean condition) { + when(repository.existsByEmail(u.getEmail())).thenReturn(condition); + } + + private void mockExistsByUsername(User u, boolean condition) { + when(repository.existsByUsername(u.getUsername())).thenReturn(condition); + } + + private void mockSave(User u) { + when(repository.save(u)).thenReturn(u); + } + + @BeforeEach + void setup() { + user = createUser("tester@notehub.com.br", "tester"); + } + + @Test + void shouldEncodePasswordAndSaveUser_whenDataIsValid() { + + mockExistsByEmail(user, false); + mockExistsByUsername(user, false); + mockSave(user); + when(encoder.encode(user.getPassword())).thenReturn("encoded"); + + User result = service.create(user); + + verify(repository).existsByEmail(user.getEmail()); + verify(repository).existsByUsername(user.getUsername()); + verify(encoder).encode("123"); + verify(repository).save(user); + + assertThat(result).isEqualTo(user); + assertThat(result.getEmail()).isEqualTo(user.getEmail()); + assertThat(result.getUsername()).isEqualTo(user.getUsername()); + assertThat(result.getDisplayName()).isEqualTo(user.getDisplayName()); + assertThat(result.getPassword()).isEqualTo("encoded"); + + } + + @Test + void shouldThrowException_whenEmailAlreadyExists() { + + mockExistsByEmail(user, true); + mockExistsByUsername(user, false); + + assertThatThrownBy(() -> service.create(user)) + .isInstanceOf(DataIntegrityViolationException.class) + .hasMessage("email"); + + verify(repository, never()).save(user); + + } + + @Test + void shouldThrowException_whenUsernameAlreadyExists() { + + mockExistsByEmail(user, false); + mockExistsByUsername(user, true); + + assertThatThrownBy(() -> service.create(user)) + .isInstanceOf(DataIntegrityViolationException.class) + .hasMessage("username"); + + verify(repository, never()).save(user); + + } + + @Test + void shouldThrowException_whenEmailAndUsernameAlreadyExist() { + + mockExistsByEmail(user, true); + mockExistsByUsername(user, true); + + assertThatThrownBy(() -> service.create(user)) + .isInstanceOf(DataIntegrityViolationException.class) + .hasMessage("both"); + + verify(repository, never()).save(user); + + } + + @Test + void shouldReturnToken_whenGeneratingActivationToken() { + when(tokenService.generateActivationToken(user)).thenReturn("token"); + String token = service.generateActivationToken(user); + assertThat(token).isEqualTo("token"); + } + + @Test + void shouldSetActiveTrueAndLogHistory_whenActivatingUser() { + + User inactive = createUser("inactive@notehub.com.br", "inactive"); + inactive.setActive(false); + + when(repository.findById(inactive.getId())).thenReturn(Optional.of(inactive)); + mockSave(inactive); + + service.activate(inactive.getId()); + + verify(repository).save(inactive); + verify(historian).setHistory(inactive, "active", "false", "true"); + + } + +} \ No newline at end of file diff --git a/src/test/java/br/com/notehub/implementation/user/UserDeletionTest.java b/src/test/java/br/com/notehub/implementation/user/UserDeletionTest.java new file mode 100644 index 0000000..ddf169a --- /dev/null +++ b/src/test/java/br/com/notehub/implementation/user/UserDeletionTest.java @@ -0,0 +1,84 @@ +package br.com.notehub.implementation.user; + +import br.com.notehub.application.implementation.user.UserServiceImpl; +import br.com.notehub.domain.note.NoteService; +import br.com.notehub.domain.user.User; +import br.com.notehub.domain.user.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserDeletionTest { + + @InjectMocks + private UserServiceImpl service; + + @Mock + private UserRepository repository; + + @Mock + private NoteService noteService; + + @Mock + private PasswordEncoder encoder; + + private User user; + + private void mockFindById(User u) { + when(repository.findById(u.getId())).thenReturn(Optional.of(u)); + } + + private void mockMatches(boolean condition) { + when(encoder.matches(anyString(), anyString())).thenReturn(condition); + } + + @BeforeEach + void setup() { + user = new User("tester@notehub.com.br", "tester", "TESTER", "123"); + user.setId(UUID.randomUUID()); + user.setActive(true); + } + + @Test + void shouldDeleteUser_whenPasswordMatches() { + + mockFindById(user); + mockMatches(true); + + service.delete(user.getId(), user.getPassword()); + + verify(repository).findById(user.getId()); + verify(repository).delete(user); + verify(noteService).deleteAllUserHiddenNotes(user); + + } + + @Test + void shouldThrowBadCredentialsException_whenPasswordDoesNotMatch() { + + mockFindById(user); + mockMatches(false); + + assertThatThrownBy(() -> + service.delete(user.getId(), user.getPassword())) + .isInstanceOf(BadCredentialsException.class); + + verify(repository).findById(user.getId()); + verify(repository, never()).delete(user); + verify(noteService, never()).deleteAllUserHiddenNotes(user); + + } + +} \ No newline at end of file diff --git a/src/test/java/br/com/notehub/implementation/user/UserEditionTest.java b/src/test/java/br/com/notehub/implementation/user/UserEditionTest.java new file mode 100644 index 0000000..46f8277 --- /dev/null +++ b/src/test/java/br/com/notehub/implementation/user/UserEditionTest.java @@ -0,0 +1,101 @@ +package br.com.notehub.implementation.user; + +import br.com.notehub.application.implementation.user.UserServiceImpl; +import br.com.notehub.domain.history.UserHistoryService; +import br.com.notehub.domain.user.User; +import br.com.notehub.domain.user.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserEditionTest { + + @InjectMocks + private UserServiceImpl service; + + @Mock + private UserRepository repository; + + @Mock + private UserHistoryService historian; + + private User user; + + private User updateUser(String name, boolean profilePrivate) { + return new User(name, name.toUpperCase(), null, null, null, profilePrivate); + } + + private void mockFindById(User user) { + when(repository.findById(user.getId())).thenReturn(Optional.of(user)); + } + + @BeforeEach + void setup() { + user = new User("tester@notehub.com.br", "tester", "TESTER", "123"); + user.setId(UUID.randomUUID()); + user.setActive(true); + } + + @Test + void shouldSaveHistoryAndPersistChanges_whenDataIsModified() { + + User updated = updateUser("updated", true); + + mockFindById(user); + + assertThat(updated).isEqualTo(service.edit(user.getId(), updated)); + + verify(repository, times(6)).findById(user.getId()); + verify(repository, times(3)).save(user); + verify(historian).setHistory(user, "username", "tester", "updated"); + verify(historian).setHistory(user, "display_name", "TESTER", "UPDATED"); + verify(historian).setHistory(user, "profile_private", "false", "true"); + + } + + @Test + void shouldSkipSaveAndHistory_whenUserDataIsUnchanged() { + + User updated = updateUser("tester", false); + + mockFindById(user); + + assertThat(updated).isEqualTo(service.edit(user.getId(), updated)); + + verify(repository, times(6)).findById(user.getId()); + verify(repository, never()).save(user); + verify(historian, never()).setHistory(any(), any(), any(), any()); + + } + + @Test + void shouldThrowDataIntegrityViolationException_whenUsernameAlreadyExists() { + + User updated = updateUser("tester", false); + + mockFindById(user); + when(repository.findByUsername("tester")).thenReturn(Optional.of(updated)); + + assertThatThrownBy(() -> service.edit(user.getId(), updated)) + .isInstanceOf(DataIntegrityViolationException.class) + .hasMessage("username"); + + verify(repository).findById(user.getId()); + verify(repository, never()).save(user); + verify(historian, never()).setHistory(any(), any(), any(), any()); + + } + +} \ No newline at end of file diff --git a/src/test/java/br/com/notehub/implementation/user/UserRelationshipTest.java b/src/test/java/br/com/notehub/implementation/user/UserRelationshipTest.java new file mode 100644 index 0000000..c0d5e0b --- /dev/null +++ b/src/test/java/br/com/notehub/implementation/user/UserRelationshipTest.java @@ -0,0 +1,149 @@ +package br.com.notehub.implementation.user; + +import br.com.notehub.application.counter.Counter; +import br.com.notehub.application.dto.notification.MessageNotification; +import br.com.notehub.application.implementation.user.UserServiceImpl; +import br.com.notehub.domain.notification.NotificationService; +import br.com.notehub.domain.user.User; +import br.com.notehub.domain.user.UserRepository; +import br.com.notehub.infra.exception.CustomExceptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserRelationshipTest { + + @InjectMocks + private UserServiceImpl service; + + @Mock + private UserRepository repository; + + @Mock + private NotificationService notifier; + + @Mock + private Counter counter; + + private User follower; + private User following; + + private User createUser(String email, String username) { + User u = new User(email, username, username.toUpperCase(), "123"); + u.setId(UUID.randomUUID()); + u.setActive(true); + return u; + } + + private void mockfindByIdWithFollowersAndFollowing(User u) { + when(repository.findByIdWithFollowersAndFollowing(u.getId())).thenReturn(Optional.of(u)); + } + + private void mockfindByUsernameWithFollowersAndFollowing(User u) { + when(repository.findByUsernameWithFollowersAndFollowing(u.getUsername())).thenReturn(Optional.of(u)); + } + + @BeforeEach + void setup() { + follower = createUser("follower@notehub.com.br", "follower"); + following = createUser("following@notehub.com.br", "following"); + } + + @Test + void shouldFollowUserAndSendNotification_whenNotAlreadyFollowing() { + + mockfindByIdWithFollowersAndFollowing(follower); + mockfindByUsernameWithFollowersAndFollowing(following); + + service.follow(follower.getId(), following.getUsername()); + + verify(repository).findByIdWithFollowersAndFollowing(follower.getId()); + verify(repository).findByUsernameWithFollowersAndFollowing(following.getUsername()); + verify(counter).updateFollowersAndFollowingCount(eq(follower), eq(following), eq(true)); + verify(notifier).notify(eq(follower), eq(following), eq(follower), eq(MessageNotification.of(follower))); + + } + + @Test + void shouldUnfollowUserWithoutNotification_whenAlreadyFollowing() { + + mockfindByIdWithFollowersAndFollowing(follower); + mockfindByUsernameWithFollowersAndFollowing(following); + + follower.getFollowing().add(following); + following.getFollowers().add(follower); + service.unfollow(follower.getId(), following.getUsername()); + + verify(repository).findByIdWithFollowersAndFollowing(follower.getId()); + verify(repository).findByUsernameWithFollowersAndFollowing(following.getUsername()); + verify(counter).updateFollowersAndFollowingCount(eq(follower), eq(following), eq(false)); + verify(notifier, never()).notify(any(), any(), any(), any()); + + } + + @Test + void shouldThrowSelfFollowException_whenTryingToFollowSelf() { + + mockfindByIdWithFollowersAndFollowing(follower); + mockfindByUsernameWithFollowersAndFollowing(follower); + + assertThatThrownBy(() -> + service.follow(follower.getId(), follower.getUsername())) + .isInstanceOf(CustomExceptions.SelfFollowException.class); + + verify(repository).findByIdWithFollowersAndFollowing(follower.getId()); + verify(repository).findByUsernameWithFollowersAndFollowing(follower.getUsername()); + verify(counter, never()).updateFollowersAndFollowingCount(any(), any(), any(Boolean.class)); + verify(notifier, never()).notify(any(), any(), any(), any()); + + } + + @Test + void shouldThrowAlreadyFollowingException_whenFollowingUserAgain() { + + mockfindByIdWithFollowersAndFollowing(follower); + mockfindByUsernameWithFollowersAndFollowing(following); + + follower.getFollowing().add(following); + following.getFollowers().add(follower); + + assertThatThrownBy(() -> + service.follow(follower.getId(), following.getUsername())) + .isInstanceOf(CustomExceptions.AlreadyFollowingException.class); + + verify(repository).findByIdWithFollowersAndFollowing(follower.getId()); + verify(repository).findByUsernameWithFollowersAndFollowing(following.getUsername()); + verify(counter, never()).updateFollowersAndFollowingCount(any(), any(), any(Boolean.class)); + verify(notifier, never()).notify(any(), any(), any(), any()); + + } + + @Test + void shouldThrowNotFollowingException_whenUnfollowingUserNotFollowed() { + + mockfindByIdWithFollowersAndFollowing(follower); + mockfindByUsernameWithFollowersAndFollowing(following); + + assertThatThrownBy(() -> + service.unfollow(follower.getId(), following.getUsername())) + .isInstanceOf(CustomExceptions.NotFollowingException.class); + + verify(repository).findByIdWithFollowersAndFollowing(follower.getId()); + verify(repository).findByUsernameWithFollowersAndFollowing(following.getUsername()); + verify(counter, never()).updateFollowersAndFollowingCount(any(), any(), any(Boolean.class)); + verify(notifier, never()).notify(any(), any(), any(), any()); + + } + +} \ No newline at end of file diff --git a/src/test/java/br/com/notehub/implementation/user/UserRetrievalTest.java b/src/test/java/br/com/notehub/implementation/user/UserRetrievalTest.java new file mode 100644 index 0000000..7025495 --- /dev/null +++ b/src/test/java/br/com/notehub/implementation/user/UserRetrievalTest.java @@ -0,0 +1,325 @@ +package br.com.notehub.implementation.user; + +import br.com.notehub.application.implementation.user.UserServiceImpl; +import br.com.notehub.domain.history.UserHistoryService; +import br.com.notehub.domain.user.Subscription; +import br.com.notehub.domain.user.User; +import br.com.notehub.domain.user.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserRetrievalTest { + + @InjectMocks + private UserServiceImpl service; + + @Mock + private UserRepository repository; + + @Mock + private UserHistoryService historian; + + private User user; + private Pageable pageable; + + private User createUser(String email, String username) { + User u = new User(email, username, username.toUpperCase(), "123"); + u.setId(UUID.randomUUID()); + u.setActive(true); + return u; + } + + private void mockFindById(User u) { + when(repository.findById(u.getId())).thenReturn(Optional.of(u)); + } + + private void mockFindByUsername(User u) { + when(repository.findByUsername(u.getUsername())).thenReturn(Optional.of(u)); + } + + @BeforeEach + void setup() { + user = createUser("tester@notehub.com.br", "tester"); + pageable = PageRequest.of(0, 10); + } + + @Test + void shouldReturnUser_whenUsernameWasFoundAndUserIsActive() { + + mockFindByUsername(user); + + User result = service.getUser(user.getUsername()); + + verify(repository).findByUsername(user.getUsername()); + + assertThat(result).isEqualTo(user); + + } + + @Test + void shouldThrowEntityNotFoundException_whenUsernameWasNotFoundOrUserIsInactive() { + + when(repository.findByUsername(anyString())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.getUser(user.getUsername())) + .isInstanceOf(EntityNotFoundException.class); + + verify(repository).findByUsername(user.getUsername()); + + } + + @Test + void shouldReturnListOfActiveUsers() { + + when(repository.findAllByActiveTrue()).thenReturn(List.of(user)); + + List result = service.getAllActiveUsers(); + + verify(repository).findAllByActiveTrue(); + + assertThat(result).containsExactly(user); + + } + + @Test + void shouldReturnPageOfActiveUsersByQuery() { + + String query = "tester"; + Page page = new PageImpl<>(List.of(user)); + + when(repository.findAllActiveUsersByUsernameOrDisplayName(pageable, query)).thenReturn(page); + + Page result = service.findAll(pageable, query); + + verify(repository).findAllActiveUsersByUsernameOrDisplayName(pageable, query); + + assertThat(result).isEqualTo(page); + + } + + @Test + void shouldReturnPageOfUsersThatRequestedUserIsFollowing() { + + User requesting = user; + User target = createUser("target@mail.com", "target"); + User u1 = createUser("a@mail.com", "a"); + User u2 = createUser("b@mail.com", "b"); + + List users = List.of(u1, u2); + Set ids = Set.of(u1.getId(), u2.getId()); + target.getFollowing().addAll(users); + + Page expected = new PageImpl<>(users); + + mockFindById(requesting); + mockFindByUsername(target); + + when(repository.findAllByIdIn( + eq(pageable), + eq("q"), + argThat(list -> new HashSet<>(list).equals(ids)) + )).thenReturn(expected); + + Page result = service.getUserFollowing(pageable, "q", requesting.getId(), target.getUsername()); + + verify(repository).findById(requesting.getId()); + verify(repository).findByUsername(target.getUsername()); + verify(repository).findAllByIdIn( + eq(pageable), + eq("q"), + argThat(list -> new HashSet<>(list).equals(ids)) + ); + + assertThat(result).isEqualTo(expected); + + } + + @Test + void shouldReturnPageOfUsersThatFollowsRequestedUser() { + + User requesting = user; + User target = createUser("target@mail.com", "target"); + User u1 = createUser("a@mail.com", "a"); + User u2 = createUser("b@mail.com", "b"); + + List users = List.of(u1, u2); + Set ids = Set.of(u1.getId(), u2.getId()); + target.getFollowers().addAll(users); + + Page expected = new PageImpl<>(users); + + mockFindById(requesting); + mockFindByUsername(target); + + when(repository.findAllByIdIn( + eq(pageable), + eq("q"), + argThat(list -> new HashSet<>(list).equals(ids)) + )).thenReturn(expected); + + Page result = service.getUserFollowers(pageable, "q", requesting.getId(), target.getUsername()); + + verify(repository).findById(requesting.getId()); + verify(repository).findByUsername(target.getUsername()); + verify(repository).findAllByIdIn( + eq(pageable), + eq("q"), + argThat(list -> new HashSet<>(list).equals(ids)) + ); + + assertThat(result).isEqualTo(expected); + + } + + @Test + void shouldThrowEntityNotFoundException_whenRequestingUserNotFound() { + + UUID id = UUID.randomUUID(); + + when(repository.findById(id)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> + service.getUserFollowing(pageable, "q", id, "someone")) + .isInstanceOf(EntityNotFoundException.class); + + verify(repository).findById(id); + verify(repository, never()).findByUsername(anyString()); + verify(repository, never()).findAllByIdIn(any(), anyString(), anyList()); + + } + + @Test + void shouldThrowEntityNotFoundException_whenRequestedUserNotFound() { + + mockFindById(user); + when(repository.findByUsername(anyString())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> + service.getUserFollowing(pageable, "q", user.getId(), "unknown")) + .isInstanceOf(EntityNotFoundException.class); + + verify(repository).findById(user.getId()); + verify(repository).findByUsername("unknown"); + verify(repository, never()).findAllByIdIn(any(), anyString(), anyList()); + + } + + @Test + void shouldThrowAccessDeniedException_whenAccessIsNotAllowed() { + + User requesting = createUser("req@mail.com", "req"); + User target = createUser("tar@mail.com", "tar"); + target.setProfilePrivate(true); + + mockFindById(requesting); + mockFindByUsername(target); + + assertThatThrownBy(() -> + service.getUserFollowing(pageable, "q", requesting.getId(), "tar")) + .isInstanceOf(AccessDeniedException.class); + + verify(repository).findById(requesting.getId()); + verify(repository).findByUsername("tar"); + verify(repository, never()).findAllByIdIn(any(), anyString(), anyList()); + + } + + @Test + void shouldReturnStringSetOfAllUserMutuals() { + + User u1 = createUser("a@mail.com", "a"); + User u2 = createUser("b@mail.com", "b"); + + List users = List.of(u1, u2); + + user.getFollowers().addAll(users); + user.getFollowing().addAll(users); + + when(repository.findByIdWithFollowersAndFollowing(user.getId())).thenReturn(Optional.of(user)); + + Set mutuals = service.getUserMutualConnections(user.getId()); + + verify(repository).findByIdWithFollowersAndFollowing(user.getId()); + + assertThat(mutuals).containsExactlyInAnyOrder("a", "b"); + + } + + @Test + void shouldReturnEmptySet_whenUserHasNoMutualConnections() { + + user.getFollowers().add(createUser("a@mail.com", "a")); + user.getFollowing().add(createUser("b@mail.com", "b")); + + when(repository.findByIdWithFollowersAndFollowing(user.getId())).thenReturn(Optional.of(user)); + + Set mutuals = service.getUserMutualConnections(user.getId()); + + verify(repository).findByIdWithFollowersAndFollowing(user.getId()); + + assertThat(mutuals).isEmpty(); + + } + + @Test + void shouldReturnListOfUserUsernameHistory() { + + List expected = List.of("tester", "new"); + + mockFindByUsername(user); + when(historian.getLastFiveUserDisplayName(user)).thenReturn(expected); + + List history = service.getUserDisplayNameHistory(user.getUsername()); + + assertThat(history).containsExactlyElementsOf(expected); + + } + + @Test + void shouldReturnListOfUserSubscriptions() { + + mockFindById(user); + + Set expected = Set.of(Subscription.MAINTENANCE, Subscription.RELEASE); + Set subscriptions = service.getUserSubscriptions(user.getId()); + + verify(repository).findById(user.getId()); + + assertThat(subscriptions).containsExactlyInAnyOrderElementsOf(expected); + + } + + @Test + void shouldCleanUsersWithExpiredActivationTime() { + + User u1 = createUser("a@mail.com", "a"); + User u2 = createUser("b@mail.com", "b"); + List expired = List.of(u1, u2); + + when(repository.findUsersWithExpiredActivationTime(any())).thenReturn(expired); + + service.cleanUsersWithExpiredActivationTime(); + + verify(repository).findUsersWithExpiredActivationTime(any()); + verify(repository).deleteAll(expired); + + } + +} \ No newline at end of file