feat: load dashboard data (#333)

* fix: task progress data from back-end

* chore: make backend candidate image default

* feat: load data for the dashboard home from the back-end

issue #320

* test: add tets for completedTasks and TaskProgress components

* test: fix test case in the backend

* feat: fix unauthorized requests

test: add home controller test cases

* test: add home service test cases
This commit is contained in:
2025-03-06 18:51:26 -03:00
committed by GitHub
parent 4ba3d1962d
commit 0234c77538
33 changed files with 662 additions and 167 deletions
@@ -0,0 +1,50 @@
import React from 'react';
import { act, render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import api from '../../api-service/api';
import { TasksChartResponse } from '../../types/TasksChartResponse';
import CompletedTasks from '../../components/CompletedTasks';
// Mock the Chart component
vi.mock('react-charts', () => ({
Chart: ({ options }) => <div data-testid="mocked-chart">Mocked Chart</div>
}));
vi.mock('../../api-service/api');
describe('CompletedTasks Component', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should render loading state initially', async () => {
// Create a promise that never resolves
vi.spyOn(api, 'getJSON').mockImplementation(() => new Promise(() => {}));
act(() => {
render(<CompletedTasks />);
});
expect(screen.getByText('Loading...')).toBeDefined();
});
it('should render chart with data after fetching', async () => {
const mockData: TasksChartResponse[] = [
{ day: 'S', count: 5, date: new Date() },
{ day: 'M', count: 10, date: new Date() },
];
vi.spyOn(api, 'getJSON').mockResolvedValue(mockData);
act(() => {
render(<CompletedTasks />);
});
expect(screen.getByText('Loading...')).toBeDefined();
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
expect(screen.queryByText('Loading...')).toBeNull();
expect(screen.getByText('Completed Tasks')).toBeDefined();
expect(screen.getByText('Summary from the last 7 days')).toBeDefined();
});
});
@@ -0,0 +1,33 @@
import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import api from '../../api-service/api';
import { SummaryResponse } from '../../types/SummaryResponse';
import TaskProgress from '../../components/TaskProgress';
vi.mock('../../api-service/api');
describe('TaskProgress Component', () => {
it('should render after fetching', async () => {
const mockData: SummaryResponse = {
pendingTaskCount: 354,
doneTaskCount: 555,
notesCount: 2222
};
const mockedGetJSON = vi.spyOn(api, 'getJSON').mockResolvedValue(mockData);
act(() => {
render(<TaskProgress />);
});
expect(mockedGetJSON).toHaveBeenCalled();
await waitFor(() => {
expect(screen.queryByText('Total')).toBeDefined();
expect(screen.queryByText(mockData.pendingTaskCount + mockData.doneTaskCount)).toBeDefined();
expect(screen.queryByText('Pending')).toBeDefined();
expect(screen.queryByText(mockData.pendingTaskCount)).toBeDefined();
expect(screen.queryByText('Completed')).toBeDefined();
expect(screen.queryByText(mockData.doneTaskCount)).toBeDefined();
});
});
});
+30 -1
View File
@@ -1,5 +1,6 @@
import { afterEach } from 'vitest';
import { afterEach, beforeEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import { vi } from 'vitest';
// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
@@ -21,3 +22,31 @@ class ResizeObserver {
}
window.ResizeObserver = window.ResizeObserver || ResizeObserver;
// Mock fetch globally
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue([]),
text: vi.fn().mockResolvedValue(''),
blob: vi.fn().mockResolvedValue(new Blob()),
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
headers: new Headers(),
status: 200,
statusText: 'OK',
});
beforeEach(() => {
vi.resetAllMocks();
// Reset the fetch mock with default successful response
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue([]),
text: vi.fn().mockResolvedValue(''),
blob: vi.fn().mockResolvedValue(new Blob()),
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
headers: new Headers(),
status: 200,
statusText: 'OK',
});
});
+37 -46
View File
@@ -1,60 +1,49 @@
import React from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Col, Row } from 'react-bootstrap';
import { AxisOptions, Chart } from 'react-charts';
import { TasksChartResponse } from '../../types/TasksChartResponse';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
import './style.css';
type DailyStars = {
date: string;
stars: number;
};
type Series = {
label: string;
data: DailyStars[];
data: TasksChartResponse[];
};
const data: Series[] = [
{
label: 'Completed tasks',
data: [
{
date: 'S', // Sunday
stars: 3
},
{
date: 'M', // Monday
stars: 5
},
{
date: 'T', // Tuesday
stars: 2
},
{
date: 'W', // Wednesday
stars: 4
},
{
date: 'T', // Thursday
stars: 8
},
{
date: 'F', // Friday
stars: 6
},
{
date: 'S', // Saturday
stars: 4
}
]
}
];
function CompletedTasks(): React.ReactNode {
const primaryAxis = React.useMemo((): AxisOptions<DailyStars> => ({
getValue: datum => datum.date
const [data, setData] = useState<Series[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const primaryAxis = useMemo((): AxisOptions<TasksChartResponse> => ({
getValue: datum => datum.day
}), []);
const secondaryAxes = React.useMemo((): AxisOptions<DailyStars>[] => [{
getValue: datum => datum.stars
const secondaryAxes = useMemo((): AxisOptions<TasksChartResponse>[] => [{
getValue: datum => datum.count
}], []);
const getChartData = async (): Promise<void> => {
try {
const response: TasksChartResponse[] = await api.getJSON(`${ApiConfig.homeUrl}/completed-tasks-chart`);
const chartData: Series[] = [{
label: 'Completed Tasks',
data: response
}];
setData(chartData);
}
catch (error) {
console.error('Error fetching chart data:', error);
}
finally {
setLoading(false);
}
};
useEffect(() => {
getChartData();
}, []);
return (
<div className="completed-tasks">
<Row>
@@ -75,7 +64,9 @@ function CompletedTasks(): React.ReactNode {
</Row>
<Row>
<Col xs={12} className="chart-container">
<Chart options={{ data, primaryAxis, secondaryAxes }} />
{loading
? <div>Loading...</div>
: <Chart options={{ data, primaryAxis, secondaryAxes }} />}
</Col>
</Row>
</div>
+40 -5
View File
@@ -1,8 +1,43 @@
import React from 'react';
import './style.css';
import React, { useEffect, useState } from 'react';
import { Col, Row } from 'react-bootstrap';
import { SummaryResponse } from '../../types/SummaryResponse';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
import './style.css';
/**
* Task progress component.
*
* This component displays the progress of tasks.
*
* @returns {React.ReactNode} The task progress component.
*/
function TaskProgress(): React.ReactNode {
const [completedTasks, setCompletedTasks] = useState<number>(0);
const [pendingTasks, setPendingTasks] = useState<number>(0);
const [totalTasks, setTotalTasks] = useState<number>();
/**
* Fetches the tasks progress.
*/
const fetchTasksProgress = async (): Promise<void> => {
try {
const response: SummaryResponse = await api.getJSON(`${ApiConfig.homeUrl}/summary`);
const { doneTaskCount } = response;
const { pendingTaskCount } = response;
setCompletedTasks(doneTaskCount);
setPendingTasks(pendingTaskCount);
setTotalTasks(doneTaskCount + pendingTaskCount);
}
catch (e) {
console.error(e);
}
};
useEffect(() => {
fetchTasksProgress();
}, []);
return (
<div className="completed-tasks">
<Row>
@@ -36,7 +71,7 @@ function TaskProgress(): React.ReactNode {
</svg>
<div>Total</div>
<div>2</div>
<div>{totalTasks}</div>
</div>
</Col>
<Col className="chart-container text-center">
@@ -60,7 +95,7 @@ function TaskProgress(): React.ReactNode {
</defs>
</svg>
<div>Pending</div>
<div>4</div>
<div>{pendingTasks}</div>
</div>
</Col>
<Col className="chart-container text-center">
@@ -73,7 +108,7 @@ function TaskProgress(): React.ReactNode {
</svg>
<div>Completed</div>
<div>6</div>
<div>{completedTasks}</div>
</div>
</Col>
</Row>
+1
View File
@@ -28,6 +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') {
console.warn(e.message);
}
+5
View File
@@ -0,0 +1,5 @@
export type SummaryResponse = {
pendingTaskCount: number;
doneTaskCount: number;
notesCount: number;
};
+5
View File
@@ -0,0 +1,5 @@
export type TasksChartResponse = {
date: Date;
day: string;
count: number;
};
+1 -1
View File
@@ -28,7 +28,7 @@ services:
SERVER_SERVLET_CONTEXT_PATH: /
ports:
- "8585:8585"
image: server:latest
image: server:candidate
db:
container_name: db
@@ -2,6 +2,7 @@ package br.com.tasknoteapp.server.config;
import br.com.tasknoteapp.server.filter.JwtAuthenticationFilter;
import br.com.tasknoteapp.server.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -53,6 +54,13 @@ public class SecurityConfig {
.permitAll())
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.exceptionHandling(
exceptionHandling ->
exceptionHandling.authenticationEntryPoint(
(request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: " + authException.getMessage());
}))
.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -2,6 +2,7 @@ package br.com.tasknoteapp.server.controller;
import br.com.tasknoteapp.server.response.SearchResponse;
import br.com.tasknoteapp.server.response.SummaryResponse;
import br.com.tasknoteapp.server.response.TasksChartResponse;
import br.com.tasknoteapp.server.service.HomeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -10,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -43,8 +45,8 @@ public class HomeController {
mediaType = "application/json",
schema = @Schema(implementation = SummaryResponse.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
})
public SummaryResponse getSummary() {
@@ -69,8 +71,8 @@ public class HomeController {
mediaType = "application/json",
schema = @Schema(implementation = SearchResponse.class, type = "array"))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public SearchResponse search(
@@ -84,4 +86,24 @@ public class HomeController {
String term) {
return homeService.search(term);
}
/**
* Get the data for the completed tasks chart.
*
* @return List of TasksChartResponse with the data.
*/
@GetMapping("/completed-tasks-chart")
@Operation(
summary = "Get completed tasks chart",
description = "Get the data for the completed tasks chart.",
responses = {
@ApiResponse(responseCode = "200", description = "Data successfully retrieved"),
@ApiResponse(
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public List<TasksChartResponse> getTasksChart() {
return homeService.getTasksChartData();
}
}
@@ -53,8 +53,8 @@ public class NoteController {
mediaType = "application/json",
schema = @Schema(implementation = NoteResponse.class, type = "array"))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public List<NoteResponse> getAllNotes() {
@@ -82,8 +82,8 @@ public class NoteController {
mediaType = "application/json",
schema = @Schema(implementation = NoteResponse.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -132,8 +132,8 @@ public class NoteController {
description = "Wrong or missing information",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
})
public ResponseEntity<NoteResponse> postNotes(
@@ -163,8 +163,8 @@ public class NoteController {
description = "Note successfully deleted",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -53,8 +53,8 @@ public class TaskController {
mediaType = "application/json",
schema = @Schema(implementation = TaskResponse.class, type = "array"))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public List<TaskResponse> getAllTasks() {
@@ -77,8 +77,8 @@ public class TaskController {
responseCode = "200",
description = "Return the found Task and its urls, if any."),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -118,8 +118,8 @@ public class TaskController {
mediaType = "application/json",
schema = @Schema(implementation = TaskResponse.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -168,8 +168,8 @@ public class TaskController {
description = "Wrong or missing information",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
})
public ResponseEntity<TaskResponse> postTasks(
@@ -199,8 +199,8 @@ public class TaskController {
description = "Task successfully deleted",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -45,8 +45,8 @@ public class UserController {
mediaType = "application/json",
schema = @Schema(implementation = UserResponse.class, type = "array"))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
})
public List<UserResponse> getAllUsers() {
@@ -66,8 +66,8 @@ public class UserController {
mediaType = "application/json",
schema = @Schema(implementation = UserResponse.class))),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(
responseCode = "404",
@@ -39,8 +39,8 @@ public class UserSessionController {
responses = {
@ApiResponse(responseCode = "200", description = "Session successfully refreshed"),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public JwtAuthenticationResponse refresh() {
@@ -59,8 +59,8 @@ public class UserSessionController {
responses = {
@ApiResponse(responseCode = "200", description = "Account successfully deleted"),
@ApiResponse(
responseCode = "403",
description = "Forbidden. Access Denied",
responseCode = "401",
description = "Unauthorized. Access Denied",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public UserResponse deleteAccount() {
@@ -0,0 +1,25 @@
package br.com.tasknoteapp.server.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/** This class represents a done task for a user in the database. */
@Data
@Entity
@ToString
@Table(name = "user_tasks_done")
@EqualsAndHashCode
public class UserTasksDone {
@EmbeddedId private UserTasksDonePk id;
@Column(name = "done_date")
private LocalDateTime doneDate;
}
@@ -0,0 +1,20 @@
package br.com.tasknoteapp.server.entity;
import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/** This class represents a UserTasksDone primary key. */
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Embeddable
public class UserTasksDonePk {
private Long userId;
private Long taskId;
}
@@ -9,6 +9,6 @@ import org.springframework.web.server.ResponseStatusException;
public class UserForbiddenException extends ResponseStatusException {
public UserForbiddenException() {
super(HttpStatus.FORBIDDEN, "User not authorized");
super(HttpStatus.FORBIDDEN, "Forbidden content for this User.");
}
}
@@ -36,34 +36,58 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
if (Objects.isNull(authorizationHeader) || authorizationHeader.isBlank()) {
// Skip authentication for paths that don't require it
String requestPath = request.getServletPath();
if (requestPath.startsWith("/auth/") || !requestPath.startsWith("/rest/")) {
filterChain.doFilter(request, response);
return;
}
String jwtToken = authorizationHeader;
if (authorizationHeader.startsWith("Bearer ")) {
jwtToken = authorizationHeader.substring(7);
// For protected paths, require valid authentication
if (Objects.isNull(authorizationHeader) || authorizationHeader.isBlank()) {
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: Missing authentication token");
return;
}
String email = jwtService.getEmailFromToken(jwtToken);
if (!Objects.isNull(email)
&& Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
try {
String jwtToken = authorizationHeader;
if (authorizationHeader.startsWith("Bearer ")) {
jwtToken = authorizationHeader.substring(7);
}
String email = jwtService.getEmailFromToken(jwtToken);
if (Objects.isNull(email)) {
throw new ServletException("Invalid token: email not found");
}
UserDetails user = userService.userDetailsService().loadUserByUsername(email);
if (jwtService.validateTokenAndUser(jwtToken, user)) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword(), user.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
if (!jwtService.validateTokenAndUser(jwtToken, user)) {
throw new ServletException("Invalid token for user");
}
}
filterChain.doFilter(request, response);
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword(), user.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
filterChain.doFilter(request, response);
}
catch (Exception e) {
log.error("Error authenticating user", e);
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: " + e.getMessage());
return;
}
}
}
@@ -0,0 +1,13 @@
package br.com.tasknoteapp.server.repository;
import br.com.tasknoteapp.server.entity.UserTasksDone;
import br.com.tasknoteapp.server.entity.UserTasksDonePk;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
/** This interface contains methods to access the user done tasks table in the database. */
public interface UserTasksDoneRepository extends JpaRepository<UserTasksDone, UserTasksDonePk> {
List<UserTasksDone> findAllByDoneDateAfterAndId_userId(LocalDateTime date, Long userId);
}
@@ -0,0 +1,11 @@
package br.com.tasknoteapp.server.response;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "This record represents the data for the completed tasks chart.")
public record TasksChartResponse(
@Schema(description = "The full date.") LocalDateTime date,
@Schema(description = "The day. Fri, Sat, Mon, and so on.", example = "Fri") String day,
@Schema(description = "The amount for the day", example = "1") Integer count) {}
@@ -157,13 +157,13 @@ public class AuthService {
Optional<String> currentUserEmail = authUtil.getCurrentUserEmail();
if (currentUserEmail.isEmpty()) {
log.error("Unable to get current user from the request");
throw new UserForbiddenException();
throw new UserNotFoundException();
}
Optional<UserEntity> currentUserOpt = findByEmail(currentUserEmail.get());
if (currentUserOpt.isEmpty()) {
log.error("Unable to find user by email with value: {}", currentUserEmail.get());
throw new UserForbiddenException();
throw new UserNotFoundException();
}
UserEntity currentUser = currentUserOpt.get();
@@ -1,10 +1,22 @@
package br.com.tasknoteapp.server.service;
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.entity.UserTasksDone;
import br.com.tasknoteapp.server.repository.UserTasksDoneRepository;
import br.com.tasknoteapp.server.response.NoteResponse;
import br.com.tasknoteapp.server.response.SearchResponse;
import br.com.tasknoteapp.server.response.SummaryResponse;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.response.TasksChartResponse;
import br.com.tasknoteapp.server.util.AuthUtil;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -19,6 +31,12 @@ public class HomeService {
private final NoteService noteService;
private final UserTasksDoneRepository userTasksDoneRepository;
private final AuthUtil authUtil;
private final AuthService authService;
/**
* Get summary for the home page.
*
@@ -53,4 +71,67 @@ public class HomeService {
return new SearchResponse(tasks, notes);
}
/**
* Get the data for the completed tasks chart.
*
* @return List of TasksChartResponse.
*/
public List<TasksChartResponse> getTasksChartData() {
Optional<String> currentUserEmail = authUtil.getCurrentUserEmail();
String email = currentUserEmail.orElseThrow();
UserEntity user = authService.findByEmail(email).orElseThrow();
LocalDateTime date = LocalDateTime.now();
List<UserTasksDone> tasks =
userTasksDoneRepository.findAllByDoneDateAfterAndId_userId(
date.minusDays(8L), user.getId());
log.info("Tasks finished in the last 7 days: {}", tasks.size());
if (tasks.isEmpty()) {
return createListFromDate(date);
}
Map<String, Integer> dataMap = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (UserTasksDone taskDone : tasks) {
String formattedDateTime = taskDone.getDoneDate().format(formatter);
dataMap.putIfAbsent(formattedDateTime, 0);
dataMap.put(formattedDateTime, dataMap.get(formattedDateTime) + 1);
}
for (int i = 0; i < 7; i++) {
LocalDateTime dateUpdated = date.minusDays(i);
String formattedDateTime = dateUpdated.format(formatter);
dataMap.putIfAbsent(formattedDateTime, 0);
}
List<TasksChartResponse> chartData = new ArrayList<>();
for (Map.Entry<String, Integer> entry : dataMap.entrySet()) {
log.info("Day: {}, Count: {}", entry.getKey(), entry.getValue());
LocalDate parsedDate = LocalDate.parse(entry.getKey(), formatter);
chartData.add(
new TasksChartResponse(
parsedDate.atStartOfDay(),
getDayOfWeek(parsedDate.atStartOfDay()),
entry.getValue()));
}
chartData.sort((t1, t2) -> t2.date().compareTo(t1.date()));
return chartData;
}
private List<TasksChartResponse> createListFromDate(LocalDateTime date) {
List<TasksChartResponse> list = new ArrayList<>();
for (int i = 0; i < 7; i++) {
LocalDateTime dateUpdated = date.minusDays(i);
list.add(new TasksChartResponse(dateUpdated, getDayOfWeek(dateUpdated), 0));
}
return list;
}
private String getDayOfWeek(LocalDateTime date) {
return date.getDayOfWeek().toString().substring(0, 3);
}
}
@@ -4,9 +4,12 @@ import br.com.tasknoteapp.server.entity.TaskEntity;
import br.com.tasknoteapp.server.entity.TaskUrlEntity;
import br.com.tasknoteapp.server.entity.TaskUrlEntityPk;
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.entity.UserTasksDone;
import br.com.tasknoteapp.server.entity.UserTasksDonePk;
import br.com.tasknoteapp.server.exception.TaskNotFoundException;
import br.com.tasknoteapp.server.repository.TaskRepository;
import br.com.tasknoteapp.server.repository.TaskUrlRepository;
import br.com.tasknoteapp.server.repository.UserTasksDoneRepository;
import br.com.tasknoteapp.server.request.TaskPatchRequest;
import br.com.tasknoteapp.server.request.TaskRequest;
import br.com.tasknoteapp.server.response.TaskResponse;
@@ -36,6 +39,8 @@ public class TaskService {
private final TaskUrlRepository taskUrlRepository;
private final UserTasksDoneRepository userTasksDoneRepository;
/**
* Get all tasks for the current user.
*
@@ -162,6 +167,22 @@ public class TaskService {
log.info("Task patched! Id {}", patchedTask.getId());
if (taskEntity.getDone()) {
UserTasksDone userTasksDone = new UserTasksDone();
userTasksDone.setId(new UserTasksDonePk(user.getId(), taskEntity.getId()));
userTasksDone.setDoneDate(LocalDateTime.now());
userTasksDoneRepository.save(userTasksDone);
log.info("Task done saved in the history! Id {}", taskEntity.getId());
} else {
log.info("Task undone! Id {}", taskEntity.getId());
Optional<UserTasksDone> userTasksDone =
userTasksDoneRepository.findById(new UserTasksDonePk(user.getId(), taskEntity.getId()));
if (userTasksDone.isPresent()) {
userTasksDoneRepository.delete(userTasksDone.get());
log.info("Task undone deleted from history! Id {}", taskEntity.getId());
}
}
return TaskResponse.fromEntity(patchedTask, getAllTasksUrls(taskId));
}
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS tasknote.user_tasks_done (
user_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
done_date TIMESTAMP NOT NULL,
CONSTRAINT user_tasks_done_pk PRIMARY KEY (user_id, task_id),
CONSTRAINT user_tasks_done_user_id_fk FOREIGN KEY (user_id) REFERENCES tasknote.users (id),
CONSTRAINT user_tasks_done_task_id_fk FOREIGN KEY (task_id) REFERENCES tasknote.tasks (id)
);
@@ -10,8 +10,10 @@ import br.com.tasknoteapp.server.response.NoteResponse;
import br.com.tasknoteapp.server.response.SearchResponse;
import br.com.tasknoteapp.server.response.SummaryResponse;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.response.TasksChartResponse;
import br.com.tasknoteapp.server.service.HomeService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.DisplayName;
@@ -66,7 +68,7 @@ class HomeControllerTest {
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -101,4 +103,40 @@ class HomeControllerTest {
.andExpect(jsonPath("$.notes[0].urls", Matchers.empty()))
.andReturn();
}
@Test
@DisplayName("Get tasks chart data with happy path should succeed")
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
void getTasksChart_happyPath_shouldSucceed() throws Exception {
TasksChartResponse responseOne = new TasksChartResponse(LocalDateTime.now(), "Thu", 1);
TasksChartResponse responseTwo = new TasksChartResponse(LocalDateTime.now(), "Fri", 2);
when(homeService.getTasksChartData()).thenReturn(List.of(responseOne, responseTwo));
mockMvc
.perform(
get("/rest/home/completed-tasks-chart")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].day").value("Thu"))
.andExpect(jsonPath("$[0].count").value(1))
.andExpect(jsonPath("$[1].day").value("Fri"))
.andExpect(jsonPath("$[1].count").value(2))
.andReturn();
}
@Test
@DisplayName("Get tasks chart data user not authorized should fail")
void getTasksChart_userNotAuthorized_shouldFail() throws Exception {
mockMvc
.perform(
get("/rest/home/completed-tasks-chart")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized())
.andReturn();
}
}
@@ -80,15 +80,15 @@ class NoteControllerTest {
}
@Test
@DisplayName("Get all notes with 403 forbidden request should fail")
void getAllNotes_forbidden_shouldFail() throws Exception {
@DisplayName("Get all notes with 401 unauthorized request should fail")
void getAllNotes_unauthorized_shouldFail() throws Exception {
mockMvc
.perform(
get("/rest/notes")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -158,8 +158,8 @@ class NoteControllerTest {
}
@Test
@DisplayName("Patch a note via patch request with 403 forbidden exception should fail")
void patchNote_forbidden_shouldFail() throws Exception {
@DisplayName("Patch a note via patch request with 401 unauthorized exception should fail")
void patchNote_unauthorized_shouldFail() throws Exception {
Long noteId = 123L;
final String payloadJson =
@@ -178,7 +178,7 @@ class NoteControllerTest {
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.content(payloadJson))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -242,12 +242,12 @@ class NoteControllerTest {
}
@Test
@DisplayName("Post create note with 403 forbidden request should fail")
void postNotes_forbidden_shouldFail() throws Exception {
@DisplayName("Post create note with 401 unauthorized request should fail")
void postNotes_unauthorized_shouldFail() throws Exception {
final String payloadJson =
"""
{
"description": "Forbidden"
"description": "Any description here"
}
""";
@@ -258,7 +258,7 @@ class NoteControllerTest {
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.content(payloadJson))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -281,8 +281,8 @@ class NoteControllerTest {
}
@Test
@DisplayName("Delete note with 403 request forbidden should fail")
void deleteNote_forbidden_shouldFail() throws Exception {
@DisplayName("Delete note with 401 unauthorized should fail")
void deleteNote_unauthorized_shouldFail() throws Exception {
final Long noteId = 453L;
mockMvc
@@ -291,7 +291,7 @@ class NoteControllerTest {
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -81,15 +81,15 @@ class TaskControllerTest {
}
@Test
@DisplayName("Get all tasks with 403 forbidden request should fail")
void getAllTasks_forbidden_shouldFail() throws Exception {
@DisplayName("Get all tasks with 401 unauthorized request should fail")
void getAllTasks_unauthorized_shouldFail() throws Exception {
mockMvc
.perform(
get("/rest/tasks")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -147,8 +147,8 @@ class TaskControllerTest {
}
@Test
@DisplayName("Get task by id forbidden should fail")
void getTaskById_forbidden_shouldFail() throws Exception {
@DisplayName("Get task by id unauthorized should fail")
void getTaskById_unauthorized_shouldFail() throws Exception {
Long taskId = 997L;
mockMvc
@@ -157,7 +157,7 @@ class TaskControllerTest {
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -236,8 +236,8 @@ class TaskControllerTest {
}
@Test
@DisplayName("Patch a task via patch request with 403 forbidden exception")
void patchTask_forbidden_shouldFail() throws Exception {
@DisplayName("Patch a task via patch request with 401 unauthorized exception")
void patchTask_unauthorized_shouldFail() throws Exception {
Long taskId = 111L;
final String payloadJson =
@@ -257,7 +257,7 @@ class TaskControllerTest {
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.content(payloadJson))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -269,15 +269,7 @@ class TaskControllerTest {
TaskResponse taskResponse =
new TaskResponse(
858L,
"Description patched",
false,
true,
null,
null,
"Moments ago",
"tag",
List.of());
858L, "Description patched", false, true, null, null, "Moments ago", "tag", List.of());
when(taskService.createTask(request)).thenReturn(taskResponse);
final String payloadJson =
@@ -324,12 +316,12 @@ class TaskControllerTest {
}
@Test
@DisplayName("Post create task with 403 forbidden request should fail")
void postTasks_forbidden_shouldFail() throws Exception {
@DisplayName("Post create task with 401 unauthorized request should fail")
void postTasks_unauthorized_shouldFail() throws Exception {
final String payloadJson =
"""
{
"description": "Forbidden"
"description": "Any description here"
}
""";
@@ -340,7 +332,7 @@ class TaskControllerTest {
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.content(payloadJson))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -363,8 +355,8 @@ class TaskControllerTest {
}
@Test
@DisplayName("Delete task with 403 request forbidden should fail")
void deleteTask_forbidden_shouldFail() throws Exception {
@DisplayName("Delete task with 401 unauthorized request should fail")
void deleteTask_unauthorized_shouldFail() throws Exception {
final Long taskId = 533L;
mockMvc
@@ -373,7 +365,7 @@ class TaskControllerTest {
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -50,15 +50,15 @@ class UserControllerTest {
}
@Test
@DisplayName("Get all users with 403 forbidden request should fail")
void getAllUsers_forbidden_shouldFail() throws Exception {
@DisplayName("Get all users with 401 unauthorized request should fail")
void getAllUsers_unauthorized_shouldFail() throws Exception {
mockMvc
.perform(
get("/rest/users")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -47,15 +47,15 @@ class UserSessionControllerTest {
}
@Test
@DisplayName("Refresh with 403 forbidden request should fail")
void refresh_forbidden_shouldFail() throws Exception {
@DisplayName("Refresh with 401 unauthorized request should fail")
void refresh_unauthorized_shouldFail() throws Exception {
mockMvc
.perform(
get("/rest/user-sessions/refresh")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
@@ -77,15 +77,15 @@ class UserSessionControllerTest {
}
@Test
@DisplayName("Delete account with 403 forbidden request should fail")
void deleteAccount_forbidden_shouldFail() throws Exception {
@DisplayName("Delete account with 401 unauthorized request should fail")
void deleteAccount_unauthorized_shouldFail() throws Exception {
mockMvc
.perform(
post("/rest/user-sessions/delete-account")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
.andReturn();
}
}
@@ -289,7 +289,7 @@ class AuthServiceTest {
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.empty());
Assertions.assertThrows(
UserForbiddenException.class,
UserNotFoundException.class,
() -> {
authService.getAllUsers();
});
@@ -303,7 +303,7 @@ class AuthServiceTest {
when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
Assertions.assertThrows(
UserForbiddenException.class,
UserNotFoundException.class,
() -> {
authService.getAllUsers();
});
@@ -403,12 +403,13 @@ class AuthServiceTest {
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
when(userRepository.save(any())).thenReturn(existing);
String newPassword = "TestHackedPw@difficult!#:)";
UserPatchRequest patchRequest = new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword);
UserPatchRequest patchRequest =
new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword);
when(authUtil.validatePassword(patchRequest.password())).thenReturn(Optional.empty());
UserResponse response = authService.patchUserInfo(patchRequest);
Assertions.assertNotNull(response);
@@ -1,14 +1,24 @@
package br.com.tasknoteapp.server.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.entity.UserTasksDone;
import br.com.tasknoteapp.server.entity.UserTasksDonePk;
import br.com.tasknoteapp.server.repository.UserTasksDoneRepository;
import br.com.tasknoteapp.server.response.NoteResponse;
import br.com.tasknoteapp.server.response.SearchResponse;
import br.com.tasknoteapp.server.response.SummaryResponse;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.response.TasksChartResponse;
import br.com.tasknoteapp.server.util.AuthUtil;
import java.time.LocalDateTime;
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;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -21,6 +31,12 @@ class HomeServiceTest {
@Mock private NoteService noteService;
@Mock private UserTasksDoneRepository userTasksDoneRepository;
@Mock private AuthUtil authUtil;
@Mock private AuthService authService;
private HomeService homeService;
private List<TaskResponse> tasks;
@@ -28,7 +44,8 @@ class HomeServiceTest {
@BeforeEach
void setUp() {
homeService = new HomeService(taskService, noteService);
homeService =
new HomeService(taskService, noteService, userTasksDoneRepository, authUtil, authService);
TaskResponse task1 =
new TaskResponse(2L, "Task 1", false, false, null, null, null, "tag", List.of());
@@ -48,9 +65,9 @@ class HomeServiceTest {
SummaryResponse summary = homeService.getSummary();
assertEquals(2, summary.pendingTaskCount());
assertEquals(0, summary.doneTaskCount());
assertEquals(2, summary.notesCount());
Assertions.assertEquals(2, summary.pendingTaskCount());
Assertions.assertEquals(0, summary.doneTaskCount());
Assertions.assertEquals(2, summary.notesCount());
}
@Test
@@ -61,7 +78,61 @@ class HomeServiceTest {
SearchResponse searchResponse = homeService.search(term);
assertEquals(0, searchResponse.tasks().size());
assertEquals(2, searchResponse.notes().size());
Assertions.assertEquals(0, searchResponse.tasks().size());
Assertions.assertEquals(2, searchResponse.notes().size());
}
@Test
@DisplayName("Get tasks chart data happy path should succeed")
void getTasksChartData_happyPath_shouldSucceed() {
Long userId = 1L;
Long taskId = 2L;
String userEmail = "user@domain.com";
LocalDateTime now = LocalDateTime.now();
String firstDay = now.getDayOfWeek().toString().substring(0, 3);
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(userEmail));
UserEntity userEntity = new UserEntity();
userEntity.setId(userId);
userEntity.setEmail(userEmail);
when(authService.findByEmail(userEmail)).thenReturn(Optional.of(userEntity));
UserTasksDone userTasksDone = new UserTasksDone();
userTasksDone.setId(new UserTasksDonePk(userId, taskId));
userTasksDone.setDoneDate(now);
when(userTasksDoneRepository.findAllByDoneDateAfterAndId_userId(any(), any()))
.thenReturn(List.of(userTasksDone));
List<TasksChartResponse> chartData = homeService.getTasksChartData();
Assertions.assertNotNull(chartData);
Assertions.assertEquals(7, chartData.size());
Assertions.assertEquals(firstDay, chartData.get(0).day());
}
@Test
@DisplayName("Get tasks chart data empty data should succeed")
void getTasksChartData_emptyData_shouldSucceed() {
Long userId = 1L;
String userEmail = "user@domain.com";
LocalDateTime now = LocalDateTime.now();
String firstDay = now.getDayOfWeek().toString().substring(0, 3);
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(userEmail));
UserEntity userEntity = new UserEntity();
userEntity.setId(userId);
userEntity.setEmail(userEmail);
when(authService.findByEmail(userEmail)).thenReturn(Optional.of(userEntity));
when(userTasksDoneRepository.findAllByDoneDateAfterAndId_userId(any(), any()))
.thenReturn(List.of());
List<TasksChartResponse> chartData = homeService.getTasksChartData();
Assertions.assertNotNull(chartData);
Assertions.assertEquals(7, chartData.size());
Assertions.assertEquals(firstDay, chartData.get(0).day());
}
}
@@ -10,9 +10,11 @@ import br.com.tasknoteapp.server.entity.TaskEntity;
import br.com.tasknoteapp.server.entity.TaskUrlEntity;
import br.com.tasknoteapp.server.entity.TaskUrlEntityPk;
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.entity.UserTasksDonePk;
import br.com.tasknoteapp.server.exception.TaskNotFoundException;
import br.com.tasknoteapp.server.repository.TaskRepository;
import br.com.tasknoteapp.server.repository.TaskUrlRepository;
import br.com.tasknoteapp.server.repository.UserTasksDoneRepository;
import br.com.tasknoteapp.server.request.TaskPatchRequest;
import br.com.tasknoteapp.server.request.TaskRequest;
import br.com.tasknoteapp.server.response.TaskResponse;
@@ -38,6 +40,8 @@ class TaskServiceTest {
@Mock TaskUrlRepository taskUrlRepository;
@Mock UserTasksDoneRepository userTasksDoneRepository;
private static final Long USER_ID = 123L;
private static final String USER_EMAIL = "test@domain.com";
@@ -46,7 +50,9 @@ class TaskServiceTest {
@BeforeEach
void setup() {
taskService = new TaskService(taskRepository, authService, authUtil, taskUrlRepository);
taskService =
new TaskService(
taskRepository, authService, authUtil, taskUrlRepository, userTasksDoneRepository);
}
@Test
@@ -345,6 +351,7 @@ class TaskServiceTest {
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));
@@ -354,9 +361,13 @@ class TaskServiceTest {
TaskEntity entity = new TaskEntity();
entity.setDescription("Updated description");
entity.setHighPriority(false);
entity.setDone(false);
entity.setTag(taskEntity.getTag());
when(taskRepository.save(any())).thenReturn(entity);
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);
TaskResponse patched = taskService.patchTask(taskId, patch);