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:
+2
-1
@@ -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')
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,4 +5,5 @@ export type SigninResponse = {
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
token: string;
|
||||
gravatarImageUrl: string;
|
||||
};
|
||||
|
||||
@@ -4,4 +4,5 @@ export type UserResponse = {
|
||||
email: string;
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
gravatarImageUrl: string;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user