feat: add gravatar support (#347)

* feat: add gravatar support

issue #323

chore: fix sidbar issues

* test: fix test case

* chore: fix sonar issues

* chore: fix sonar cloud issues

* test: fix test cases

* test: add more test cases

* ci: fix caddyfile and docker compose
This commit is contained in:
2025-03-11 21:01:00 -03:00
committed by GitHub
parent a65e55caae
commit e5f3e339f6
24 changed files with 590 additions and 94 deletions
+2 -1
View File
@@ -28,7 +28,8 @@
connect-src 'self' {$VITE_BACKEND_SERVER};
default-src 'self' data:;
font-src 'self' https://fonts.gstatic.com/ https://cdn.jsdelivr.net/;
frame-src 'self' img-src 'self';
frame-src 'self';
img-src 'self' https://gravatar.com/;
manifest-src 'self';
media-src 'self';
object-src 'none';
@@ -14,7 +14,8 @@ const authContextMock = {
name: 'Ricardo',
email: 'ricardo@campos.com',
admin: false,
createdAt: new Date()
createdAt: new Date(),
gravatarImageUrl: 'http://url.com'
},
checkCurrentAuthUser: vi.fn(),
signIn: vi.fn(),
@@ -0,0 +1,279 @@
// AuthProvider.test.tsx
import React, { useContext } from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, act, waitFor, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AuthProvider from '../../context/AuthProvider';
import AuthContext, { AuthContextData } from '../../context/AuthContext';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
import { API_TOKEN, USER_DATA } from '../../app-constants/app-constants';
// Mock the API service methods.
vi.mock('../../api-service/api');
// Create a helper component to consume AuthContext for testing.
const ConsumerComponent: React.FC = () => {
const {
signed,
user,
signIn,
register,
signOut,
updateUser,
checkCurrentAuthUser
} = useContext<AuthContextData>(AuthContext);
return (
<div>
<div data-testid="signed">{signed ? 'true' : 'false'}</div>
<div data-testid="user">{user ? user.name : 'none'}</div>
<button
data-testid="signIn"
onClick={async() => {
await signIn('test@example.com', 'password123');
}}
>
Sign In
</button>
<button
data-testid="register"
onClick={() => {
register('new@example.com', 'password123');
}}
>
Register
</button>
<button
data-testid="sign-out-btn"
onClick={() => {
signOut();
}}
>
Sign Out
</button>
<button
data-testid="updateUser"
onClick={() => {
updateUser({
userId: 1,
name: 'Updated User',
email: 'updated@example.com',
admin: false,
createdAt: new Date(),
gravatarImageUrl: 'http://dummyimage.com'
});
}}
>
Update User
</button>
<button
data-testid="checkCurrentAuthUser"
onClick={() => {
checkCurrentAuthUser('/some-path');
}}
>
Check Current Auth User
</button>
</div>
);
};
describe('AuthProvider', () => {
// Reset DOM and mocks for each test.
beforeEach(() => {
localStorage.clear();
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it('should render the default context values', () => {
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
expect(getByTestId('signed').textContent).toBe('false');
expect(getByTestId('user').textContent).toBe('none');
});
it('should sign in a user successfully', async () => {
// Create a fake sign-in response.
const fakeResponse = {
userId: '123',
name: 'Test User',
email: 'test@example.com',
admin: false,
createdAt: new Date(),
token: 'dummy-token',
gravatarImageUrl: 'http://dummyimage.com'
};
vi.spyOn(api, 'postJSON').mockResolvedValue(fakeResponse);
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
await act(async () => {
userEvent.click(getByTestId('signIn'));
});
await waitFor(() =>
expect(getByTestId('signed').textContent).toBe('true')
);
expect(getByTestId('user').textContent).toBe('Test User');
// LocalStorage should have API_TOKEN and USER_DATA set.
expect(localStorage.getItem(API_TOKEN)).toBe('dummy-token');
expect(localStorage.getItem(USER_DATA)).not.toBeNull();
});
it('should register a new user successfully', async () => {
const fakeResponse = {
token: 'register-token',
userId: '456',
name: 'New User',
email: 'new@example.com',
admin: false,
createdAt: new Date(),
gravatarImageUrl: 'http://dummyimage.com'
};
vi.spyOn(api, 'putJSON').mockResolvedValue(fakeResponse);
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
await act(async () => {
userEvent.click(getByTestId('register'));
});
await waitFor(() =>
expect(getByTestId('signed').textContent).toBe('true')
);
expect(getByTestId('user').textContent).toBe('New User');
expect(localStorage.getItem(API_TOKEN)).toBe('register-token');
expect(localStorage.getItem(USER_DATA)).not.toBeNull();
});
it('should sign out a user', async () => {
// Pre-populate localStorage to simulate a signed-in state.
localStorage.setItem(API_TOKEN, 'dummy-token');
localStorage.setItem(
USER_DATA,
JSON.stringify({
userId: '123',
name: 'Test User',
email: 'test@example.com',
admin: false,
createdAt: new Date(),
gravatarImageUrl: 'http://dummyimage.com'
})
);
const fakeResponse = {
userId: '123',
name: 'Test User',
email: 'test@example.com',
admin: false,
createdAt: new Date(),
token: 'dummy-token',
gravatarImageUrl: 'http://dummyimage.com'
};
vi.spyOn(api, 'postJSON').mockResolvedValue(fakeResponse);
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
// Make sure that the context initially renders with the signed user by triggering a signIn.
await act(async () => {
userEvent.click(getByTestId('signIn'));
});
await waitFor(() =>
expect(getByTestId('signed').textContent).toBe('true')
);
// Now sign out
await act(async () => {
userEvent.click(getByTestId('sign-out-btn'));
});
await waitFor(() =>
expect(getByTestId('signed').textContent).toBe('false')
);
expect(getByTestId('user').textContent).toBe('none');
// LocalStorage items should be removed.
expect(localStorage.getItem(API_TOKEN)).toBeNull();
expect(localStorage.getItem(USER_DATA)).toBeNull();
});
it('should update user in context and localStorage', async () => {
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
await act(async () => {
userEvent.click(getByTestId('updateUser'));
});
await waitFor(() => {
act(() => {
expect(getByTestId('user').textContent).toBe('Updated User')
})
const savedUser = localStorage.getItem(USER_DATA);
expect(savedUser).not.toBeNull();
if (savedUser) {
const parsedUser = JSON.parse(savedUser);
expect(parsedUser.name).toBe('Updated User');
}
});
});
it('should call fetchCurrentSession when checking current auth user', async () => {
const fakeResponse = {
token: 'refresh-token',
userId: '789',
name: 'Refreshed User',
email: 'refreshed@example.com',
admin: false,
createdAt: new Date().toISOString(),
gravatarImageUrl: 'http://dummyimage.com'
};
vi.spyOn(api, 'getJSON').mockResolvedValue(fakeResponse);
// Store API_TOKEN so that fetchCurrentSession runs the refresh logic.
localStorage.setItem(API_TOKEN, 'dummy');
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);
await waitFor(() => {
userEvent.click(getByTestId('checkCurrentAuthUser'));
});
await waitFor(() =>
expect(localStorage.getItem(API_TOKEN)).toBe('refresh-token')
);
});
});
+2 -2
View File
@@ -8,7 +8,6 @@ import AuthContext from '../../context/AuthContext';
import i18n from '../../i18n';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
import TaskNoteRequest from '../../types/TaskNoteRequest';
import { NoteResponse } from '../../types/NoteResponse';
vi.mock('../../api-service/api');
@@ -35,7 +34,8 @@ const authContextMock = {
name: 'Ricardo',
email: 'test@example.com',
admin: false,
createdAt: new Date()
createdAt: new Date(),
gravatarImageUrl: 'http://url.com'
},
checkCurrentAuthUser: vi.fn(),
signIn: vi.fn(),
+2 -1
View File
@@ -34,7 +34,8 @@ const authContextMock = {
name: 'Ricardo',
email: 'test@example.com',
admin: false,
createdAt: new Date()
createdAt: new Date(),
gravatarImageUrl: 'http://url.com'
},
checkCurrentAuthUser: vi.fn(),
signIn: vi.fn(),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

+1 -2
View File
@@ -3,7 +3,6 @@ import { Nav } from 'react-bootstrap';
import { NavLink } from 'react-router';
import { useTranslation } from 'react-i18next';
import AuthContext from '../../context/AuthContext';
import UserIcon from '../../assets/user.png';
import NavButton from '../NavButton';
import SidebarIcon from '../SidebarIcon';
import { env } from '../../env';
@@ -43,7 +42,7 @@ function Sidebar(): React.ReactNode {
return (
<div className="d-flex flex-column vh-100 bg-light sidebar">
<div className="sidebar-header plus-jakarta-sans-bold">
<img width="45" src={UserIcon} alt="User icon" />
<img src={`https://gravatar.com/avatar/${user?.gravatarImageUrl}.jpg`} alt="User icon" />
<span>{user?.name ? user?.name : 'User'}</span>
</div>
+6 -1
View File
@@ -1,9 +1,12 @@
.sidebar {
width: 276px;
background-color: #fff;
position: absolute;
position: fixed;
left: 0;
top: 0;
bottom: 0;
height: 100vh;
overflow-y: auto;
}
.sidebar-header {
margin-left: 28px;
@@ -20,6 +23,8 @@
}
.sidebar-header img {
margin-right: 1rem;
border-radius: 50%;
object-fit: cover;
}
.sidebar-menu-header {
+5 -4
View File
@@ -28,8 +28,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
}
catch (e) {
if (e instanceof Error) {
// FIXME here
if (e.message !== 'No saved token!' && e.message !== 'Forbidden! Access denied') {
if (e.message !== 'No saved token!') {
console.warn(e.message);
}
}
@@ -82,7 +81,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
name: registerResponse.name,
email: registerResponse.email,
admin: registerResponse.admin,
createdAt: new Date(registerResponse.createdAt)
createdAt: new Date(registerResponse.createdAt),
gravatarImageUrl: registerResponse.gravatarImageUrl
};
setSigned(true);
@@ -107,7 +107,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
name: registerResponse.name,
email: registerResponse.email,
admin: registerResponse.admin,
createdAt: new Date(registerResponse.createdAt)
createdAt: new Date(registerResponse.createdAt),
gravatarImageUrl: registerResponse.gravatarImageUrl
};
setSigned(true);
+1
View File
@@ -5,4 +5,5 @@ export type SigninResponse = {
admin: boolean;
createdAt: Date;
token: string;
gravatarImageUrl: string;
};
+1
View File
@@ -4,4 +4,5 @@ export type UserResponse = {
email: string;
admin: boolean;
createdAt: Date;
gravatarImageUrl: string;
};
+3 -2
View File
@@ -204,9 +204,10 @@ function Account(): React.ReactNode {
If you want to display your picture, we support Gravatar.
Please head to
{' '}
<a href="#">Gravatar</a>
<a href="https://gravatar.com" target="_blank" rel="noreferrer">Gravatar</a>
{' '}
to register or update your profile picture.
to register or update your profile picture. Once updated, please wait a few
minutes to see it here.
</p>
</div>
</Col>
+9 -1
View File
@@ -267,10 +267,18 @@ function NoteAdd(): React.ReactNode {
<Button
variant="primary"
type="submit"
className="w-100 mt-3"
className="mt-3"
>
{t('note_form_submit')}
</Button>
<Button
variant="outline-primary"
type="button"
className="ms-2 mt-3"
onClick={() => navigate('/notes')}
>
Cancel
</Button>
</Form>
</Card.Body>
+8
View File
@@ -283,6 +283,14 @@ function TaskAdd(): React.ReactNode {
<Button variant="primary" type="submit">
{t('task_form_submit')}
</Button>
<Button
variant="outline-primary"
type="button"
className="ms-2"
onClick={() => navigate('/tasks')}
>
Cancel
</Button>
</Form>
</Card.Body>
</Card>
+2 -2
View File
@@ -9,7 +9,7 @@ services:
environment:
VITE_BACKEND_SERVER: http://localhost:8585
VITE_BUILD: snapshot
image: client:latest
image: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
ports:
- "5000:5000"
@@ -28,7 +28,7 @@ services:
SERVER_SERVLET_CONTEXT_PATH: /
ports:
- "8585:8585"
image: server:candidate
image: ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate
db:
container_name: db
@@ -26,7 +26,7 @@ public class LoginRequest {
String password;
public String email() {
return email.toLowerCase();
return email.trim().toLowerCase();
}
public String password() {
@@ -3,6 +3,7 @@ package br.com.tasknoteapp.server.response;
import br.com.tasknoteapp.server.entity.UserEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.Optional;
/** This record represents a User Response object. */
@Schema(description = "This record represents a User Response object.")
@@ -16,7 +17,8 @@ public record UserResponse(
@Schema(
description = "The inactivated date and time of the user",
example = "2023-01-01T00:00:00")
LocalDateTime inactivatedAt) {
LocalDateTime inactivatedAt,
@Schema(description = "The gravatar image URL, if any") String gravatarImageUrl) {
/**
* Create a {@link UserResponse} instance from a {@link UserEntity}.
@@ -24,13 +26,14 @@ public record UserResponse(
* @param user The user entity instance with user info to be used as source.
* @return UserResponse instance.
*/
public static UserResponse fromEntity(UserEntity user) {
public static UserResponse fromEntity(UserEntity user, Optional<String> gravatarUrl) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAdmin(),
user.getCreatedAt(),
user.getInactivatedAt());
user.getInactivatedAt(),
gravatarUrl.orElse(null));
}
}
@@ -3,6 +3,7 @@ package br.com.tasknoteapp.server.response;
import br.com.tasknoteapp.server.entity.UserEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.Optional;
/** This record represents a User Response object. */
@Schema(description = "This record represents a User Response with token object.")
@@ -17,6 +18,7 @@ public record UserResponseWithToken(
description = "The inactivated date and time of the user",
example = "2023-01-01T00:00:00")
LocalDateTime inactivatedAt,
@Schema(description = "The gravatar image URL, if any") String gravatarImageUrl,
@Schema(description = "The token created upon login") String token) {
/**
@@ -26,7 +28,7 @@ public record UserResponseWithToken(
* @param token The token created upon registration or login.
* @return UserResponse instance.
*/
public static UserResponseWithToken fromEntity(UserEntity user, String token) {
public static UserResponseWithToken fromEntity(UserEntity user, String token, Optional<String> gravatarUrl) {
return new UserResponseWithToken(
user.getId(),
user.getName(),
@@ -34,6 +36,7 @@ public record UserResponseWithToken(
user.getAdmin(),
user.getCreatedAt(),
user.getInactivatedAt(),
gravatarUrl.orElse(null),
token);
}
}
@@ -16,6 +16,9 @@ import br.com.tasknoteapp.server.response.UserResponse;
import br.com.tasknoteapp.server.response.UserResponseWithToken;
import br.com.tasknoteapp.server.util.AuthUtil;
import jakarta.transaction.Transactional;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -79,7 +82,7 @@ public class AuthService {
String token = jwtService.generateToken(user.getEmail());
log.info("User created! Token {}", token);
return UserResponseWithToken.fromEntity(user, token);
return UserResponseWithToken.fromEntity(user, token, getGravatarImageUrl(login.email()));
}
/**
@@ -133,7 +136,8 @@ public class AuthService {
log.info("User authenticated! Token {}", token);
userPwdLimitRepository.deleteAllForUser(user.get().getId());
return UserResponseWithToken.fromEntity(user.get(), token);
return UserResponseWithToken.fromEntity(
user.get(), token, getGravatarImageUrl(login.email()));
} catch (BadCredentialsException e) {
log.error("BadCredentialsException when logging in user {}", user.get().getId());
@@ -176,7 +180,8 @@ public class AuthService {
List<UserEntity> users = userRepository.findAll();
List<UserResponse> usersResponse = new ArrayList<>(users.size());
users.forEach((u) -> usersResponse.add(UserResponse.fromEntity(u)));
users.forEach(
u -> usersResponse.add(UserResponse.fromEntity(u, getGravatarImageUrl(u.getEmail()))));
log.info("{} user(s) found!", usersResponse.size());
return usersResponse;
@@ -216,9 +221,15 @@ public class AuthService {
userPwdLimitRepository.deleteAllForUser(currentUser.getId());
userRepository.delete(currentUser);
return UserResponse.fromEntity(currentUser);
return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email));
}
/**
* Patches a user allowing him to update his information.
*
* @param patchRequest An instance of {@link UserPatchRequest} with the user data.
* @return UserResponse containing the updated info.
*/
@Transactional
public UserResponse patchUserInfo(UserPatchRequest patchRequest) {
Optional<String> currentUserEmail = authUtil.getCurrentUserEmail();
@@ -255,7 +266,31 @@ public class AuthService {
userRepository.save(currentUser);
}
return UserResponse.fromEntity(currentUser);
return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email));
}
private Optional<String> getGravatarImageUrl(String email) {
email = email.toLowerCase().trim();
log.info("Current user email: {}", email);
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(email.toLowerCase().getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
log.info("Email hashed: {}", hexString);
return Optional.of(hexString.toString());
} catch (NoSuchAlgorithmException | NullPointerException e) {
log.error("NoSuchAlgorithmException or NullPointerException", e.getMessage());
}
return Optional.empty();
}
private void checkLoginAttemptLimit(Long userId) {
@@ -17,6 +17,7 @@ import br.com.tasknoteapp.server.util.AuthUtil;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -132,10 +133,9 @@ public class TaskService {
if (!Objects.isNull(patch.done())) {
taskEntity.setDone(patch.done());
}
taskEntity.setDueDate(null);
if (!Objects.isNull(patch.dueDate())) {
taskEntity.setDueDate(LocalDate.parse(patch.dueDate()));
}
patchDueDate(taskEntity, patch);
taskEntity.setHighPriority(false);
if (!Objects.isNull(patch.highPriority())) {
taskEntity.setHighPriority(patch.highPriority());
@@ -147,21 +147,7 @@ public class TaskService {
taskEntity.setLastUpdate(LocalDateTime.now());
List<TaskUrlEntity> urlsToDelete = taskUrlRepository.findAllById_taskId(taskId);
if (!urlsToDelete.isEmpty()) {
taskUrlRepository.deleteAllById_taskId(taskId);
log.info("Deleted {} urls from task {}", urlsToDelete.size(), taskId);
} else {
log.info("No urls to delete for task {}", taskId);
}
if (!Objects.isNull(patch.urls())) {
List<String> urlListToAdd =
patch.urls().stream().filter(u -> !u.isBlank()).map(String::trim).toList();
saveUrls(taskEntity, urlListToAdd);
} else {
log.info("No urls to add for task {}", taskId);
}
patchTaskUrl(taskEntity, patch);
TaskEntity patchedTask = taskRepository.save(taskEntity);
@@ -258,4 +244,34 @@ public class TaskService {
taskUrlRepository.saveAll(tasksUrl);
log.info("Added {} urls from task {}", tasksUrl.size(), taskEntity.getId());
}
private void patchDueDate(TaskEntity taskEntity, TaskPatchRequest patch) {
taskEntity.setDueDate(null);
if (!Objects.isNull(patch.dueDate()) && !patch.description().isBlank()) {
try {
taskEntity.setDueDate(LocalDate.parse(patch.dueDate()));
} catch (DateTimeParseException e) {
log.error("Unable to parse the provided date: {}: {}", patch.dueDate(), e.getMessage(), e);
}
}
}
private void patchTaskUrl(TaskEntity taskEntity, TaskPatchRequest patch) {
Long taskId = taskEntity.getId();
List<TaskUrlEntity> urlsToDelete = taskUrlRepository.findAllById_taskId(taskId);
if (!urlsToDelete.isEmpty()) {
taskUrlRepository.deleteAllById_taskId(taskId);
log.info("Deleted {} urls from task {}", urlsToDelete.size(), taskId);
} else {
log.info("No urls to delete for task {}", taskId);
}
if (!Objects.isNull(patch.urls())) {
List<String> urlListToAdd =
patch.urls().stream().filter(u -> !u.isBlank()).map(String::trim).toList();
saveUrls(taskEntity, urlListToAdd);
} else {
log.info("No urls to add for task {}", taskId);
}
}
}
@@ -37,7 +37,7 @@ class AuthenticationControllerTest {
UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, token);
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
when(authService.signUpNewUser(request)).thenReturn(response);
String jsonString =
@@ -71,7 +71,7 @@ class AuthenticationControllerTest {
UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, token);
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
when(authService.signUpNewUser(request)).thenReturn(response);
String jsonString =
@@ -127,7 +127,7 @@ class AuthenticationControllerTest {
UserResponseWithToken response =
new UserResponseWithToken(
123L, null, request.email(), false, LocalDateTime.now(), null, token);
123L, null, request.email(), false, LocalDateTime.now(), null, null, token);
when(authService.signInUser(request)).thenReturn(response);
String jsonString =
@@ -33,7 +33,7 @@ class UserControllerTest {
@DisplayName("Get all users happy path should succeed")
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void getAllUsers_happyPath_shouldSucceed() throws Exception {
UserResponse userResponse = new UserResponse(1L, "John", "email@test.com", false, null, null);
UserResponse userResponse = new UserResponse(1L, "John", "email@test.com", false, null, null, null);
when(authService.getAllUsers()).thenReturn(List.of(userResponse));
mockMvc
@@ -66,7 +66,7 @@ class UserControllerTest {
@DisplayName("Patch user info happy path should succeed")
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void patchUserInfo_happyPath_shouldSucceed() throws Exception {
UserResponse response = new UserResponse(1L, "John", "email@example.com", false, null, null);
UserResponse response = new UserResponse(1L, "John", "email@example.com", false, null, null, null);
UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null);
when(authService.patchUserInfo(request)).thenReturn(response);
@@ -63,7 +63,7 @@ class UserSessionControllerTest {
@DisplayName("Delete account happy path should succeed")
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void deleteAccount_happyPath_shouldSucceed() throws Exception {
UserResponse response = new UserResponse(1L, "John", "email@test.com", false, null, null);
UserResponse response = new UserResponse(1L, "John", "email@test.com", false, null, null, null);
when(userSessionService.deleteCurrentUserAccount()).thenReturn(response);
mockMvc
@@ -1,5 +1,11 @@
package br.com.tasknoteapp.server.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
@@ -19,9 +25,10 @@ import br.com.tasknoteapp.server.request.TaskPatchRequest;
import br.com.tasknoteapp.server.request.TaskRequest;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.util.AuthUtil;
import br.com.tasknoteapp.server.util.TimeAgoUtil;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -77,11 +84,11 @@ class TaskServiceTest {
TaskResponse taskResponse = taskService.getTaskById(taskId);
Assertions.assertNotNull(taskResponse);
Assertions.assertEquals(taskEntity.getId(), taskResponse.id());
Assertions.assertEquals(taskEntity.getDescription(), taskResponse.description());
Assertions.assertEquals(taskEntity.getHighPriority(), taskResponse.highPriority());
Assertions.assertEquals(taskEntity.getTag(), taskResponse.tag());
assertNotNull(taskResponse);
assertEquals(taskEntity.getId(), taskResponse.id());
assertEquals(taskEntity.getDescription(), taskResponse.description());
assertEquals(taskEntity.getHighPriority(), taskResponse.highPriority());
assertEquals(taskEntity.getTag(), taskResponse.tag());
}
@Test
@@ -98,11 +105,7 @@ class TaskServiceTest {
when(taskRepository.findById(taskId)).thenReturn(Optional.empty());
Assertions.assertThrows(
TaskNotFoundException.class,
() -> {
taskService.getTaskById(taskId);
});
assertThrows(TaskNotFoundException.class, () -> taskService.getTaskById(taskId));
}
@Test
@@ -125,8 +128,8 @@ class TaskServiceTest {
taskService.createTask(request);
Assertions.assertNotNull(entity);
Assertions.assertEquals("development", entity.getTag());
assertNotNull(entity);
assertEquals("development", entity.getTag());
}
@Test
@@ -149,8 +152,8 @@ class TaskServiceTest {
taskService.createTask(request);
Assertions.assertNotNull(entity);
Assertions.assertEquals("development", entity.getTag());
assertNotNull(entity);
assertEquals("development", entity.getTag());
}
@Test
@@ -174,8 +177,8 @@ class TaskServiceTest {
taskService.createTask(request);
Assertions.assertNotNull(entity);
Assertions.assertEquals("development", entity.getTag());
assertNotNull(entity);
assertEquals("development", entity.getTag());
}
@Test
@@ -200,8 +203,8 @@ class TaskServiceTest {
TaskResponse response = taskService.createTask(request);
Assertions.assertNotNull(response);
Assertions.assertTrue(response.urls().isEmpty());
assertNotNull(response);
assertTrue(response.urls().isEmpty());
}
@Test
@@ -226,8 +229,8 @@ class TaskServiceTest {
TaskResponse response = taskService.createTask(request);
Assertions.assertNotNull(response);
Assertions.assertTrue(response.urls().isEmpty());
assertNotNull(response);
assertTrue(response.urls().isEmpty());
}
@Test
@@ -257,9 +260,9 @@ class TaskServiceTest {
TaskResponse response = taskService.createTask(request);
Assertions.assertNotNull(response);
Assertions.assertFalse(response.urls().isEmpty());
Assertions.assertEquals("debian.org", response.urls().get(0));
assertNotNull(response);
assertFalse(response.urls().isEmpty());
assertEquals("debian.org", response.urls().get(0));
}
@Test
@@ -280,9 +283,9 @@ class TaskServiceTest {
List<TaskResponse> responses = taskService.getAllTasks();
Assertions.assertFalse(responses.isEmpty());
Assertions.assertEquals(1, responses.size());
Assertions.assertEquals(entity.getTag(), responses.get(0).tag());
assertFalse(responses.isEmpty());
assertEquals(1, responses.size());
assertEquals(entity.getTag(), responses.get(0).tag());
}
@Test
@@ -328,11 +331,7 @@ class TaskServiceTest {
when(taskRepository.findById(taskId)).thenReturn(Optional.empty());
Assertions.assertThrows(
TaskNotFoundException.class,
() -> {
taskService.deleteTask(taskId);
});
assertThrows(TaskNotFoundException.class, () -> taskService.deleteTask(taskId));
}
@Test
@@ -358,22 +357,156 @@ class TaskServiceTest {
when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of());
TaskEntity entity = new TaskEntity();
entity.setDescription("Updated description");
entity.setHighPriority(false);
entity.setDone(false);
entity.setTag(taskEntity.getTag());
when(taskRepository.save(any())).thenReturn(entity);
String dueDate = "2026-12-31";
String timeLeft = TimeAgoUtil.formatDueDate(LocalDate.parse(dueDate));
TaskEntity savedTask = new TaskEntity();
savedTask.setDescription("Test task updated");
savedTask.setHighPriority(false);
savedTask.setDueDate(LocalDate.parse(dueDate));
savedTask.setDone(true);
savedTask.setTag(taskEntity.getTag());
when(taskRepository.save(any())).thenReturn(savedTask);
UserTasksDonePk pk = new UserTasksDonePk(USER_ID, taskId);
when(userTasksDoneRepository.findById(pk)).thenReturn(Optional.empty());
TaskPatchRequest patch =
new TaskPatchRequest("Updated description", null, null, null, false, null);
new TaskPatchRequest("Test task updated", true, null, dueDate, false, "test");
TaskResponse patched = taskService.patchTask(taskId, patch);
Assertions.assertNotNull(patched);
Assertions.assertEquals("Updated description", patched.description());
Assertions.assertFalse(patched.highPriority());
assertNotNull(patched);
assertEquals("Test task updated", patched.description());
assertTrue(patched.done());
assertEquals(timeLeft, patched.dueDateFmt());
assertFalse(patched.highPriority());
assertEquals("test", patched.tag());
assertTrue(patched.urls().isEmpty());
}
@Test
@DisplayName("Patch a task with url it should succeed")
void patchTask_withUrl_shouldSucceed() {
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL));
UserEntity userEntity = new UserEntity();
userEntity.setId(USER_ID);
userEntity.setEmail(USER_EMAIL);
when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity));
Long taskId = 2525L;
TaskEntity taskEntity = new TaskEntity();
taskEntity.setId(taskId);
taskEntity.setDescription("Test task");
taskEntity.setHighPriority(true);
taskEntity.setDone(false);
taskEntity.setTag("test");
taskEntity.setUser(userEntity);
when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity));
TaskUrlEntity urlEntity = new TaskUrlEntity();
urlEntity.setId(new TaskUrlEntityPk(taskId, "www.url.com"));
when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of(urlEntity));
doNothing().when(taskUrlRepository).deleteAllById_taskId(taskId);
String dueDate = "2026-12-31";
String timeLeft = TimeAgoUtil.formatDueDate(LocalDate.parse(dueDate));
TaskEntity savedTask = new TaskEntity();
savedTask.setDescription("Test task updated");
savedTask.setHighPriority(false);
savedTask.setDueDate(LocalDate.parse(dueDate));
savedTask.setDone(true);
savedTask.setTag(taskEntity.getTag());
when(taskRepository.save(any())).thenReturn(savedTask);
UserTasksDonePk pk = new UserTasksDonePk(USER_ID, taskId);
when(userTasksDoneRepository.findById(pk)).thenReturn(Optional.empty());
String url = "http://test.com";
TaskPatchRequest patch =
new TaskPatchRequest("Test task updated", true, List.of(url), dueDate, false, "test");
when(taskUrlRepository.saveAll(any())).thenReturn(List.of());
TaskResponse patched = taskService.patchTask(taskId, patch);
assertNotNull(patched);
assertEquals("Test task updated", patched.description());
assertTrue(patched.done());
assertEquals(timeLeft, patched.dueDateFmt());
assertFalse(patched.highPriority());
assertEquals("test", patched.tag());
assertFalse(patched.urls().isEmpty());
}
@Test
@DisplayName("Patch a task with a not found task should fail")
void patchTask_taskNotFound_shouldFail() {
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL));
UserEntity userEntity = new UserEntity();
userEntity.setId(USER_ID);
userEntity.setEmail(USER_EMAIL);
when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity));
Long taskId = 2525L;
when(taskRepository.findById(taskId)).thenReturn(Optional.empty());
TaskPatchRequest patch =
new TaskPatchRequest("Test task updated", true, null, "2025-12-31", false, "test");
assertThrows(TaskNotFoundException.class, () -> taskService.patchTask(taskId, patch));
}
@Test
@DisplayName("Patch a task with a due date parse exception should fail")
void patchTask_dueDateParseException_shouldFail() {
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL));
UserEntity userEntity = new UserEntity();
userEntity.setId(USER_ID);
userEntity.setEmail(USER_EMAIL);
when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity));
Long taskId = 2525L;
TaskEntity taskEntity = new TaskEntity();
taskEntity.setId(taskId);
taskEntity.setDescription("Test task");
taskEntity.setHighPriority(true);
taskEntity.setDone(false);
taskEntity.setTag("test");
taskEntity.setUser(userEntity);
when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity));
when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of());
TaskEntity savedTask = new TaskEntity();
savedTask.setDescription("Test task updated");
savedTask.setHighPriority(false);
savedTask.setDone(true);
savedTask.setTag(taskEntity.getTag());
when(taskRepository.save(any())).thenReturn(savedTask);
UserTasksDonePk pk = new UserTasksDonePk(USER_ID, taskId);
when(userTasksDoneRepository.findById(pk)).thenReturn(Optional.empty());
// wrong due date
String dueDate = "2026-31-31";
TaskPatchRequest patch =
new TaskPatchRequest("Test task updated", true, null, dueDate, false, "test");
TaskResponse patched = taskService.patchTask(taskId, patch);
assertNotNull(patched);
assertEquals("Test task updated", patched.description());
assertTrue(patched.done());
assertNull(patched.dueDate());
assertNull(patched.dueDateFmt());
assertFalse(patched.highPriority());
assertEquals("test", patched.tag());
assertTrue(patched.urls().isEmpty());
}
}