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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type SummaryResponse = {
|
||||
pendingTaskCount: number;
|
||||
doneTaskCount: number;
|
||||
notesCount: number;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TasksChartResponse = {
|
||||
date: Date;
|
||||
day: string;
|
||||
count: number;
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
+43
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user