From 6a3ca97619db690602529af56f3702693d12e703 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 23:49:04 +0000 Subject: [PATCH 1/3] Initial plan From b8b30f18a18fa4e3c901185653db253e100f0d09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 23:54:04 +0000 Subject: [PATCH 2/3] feat: refresh and reload current user profile Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/3f01367a-baee-4bbe-8f66-f04da58d485f Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> --- .../__test__/context/AuthProvider.test.tsx | 45 +++++++++++++++---- client/src/api-service/apiConfig.ts | 4 +- client/src/context/AuthProvider.tsx | 5 ++- .../server/controller/UserController.java | 10 +++++ .../server/service/AuthService.java | 11 +++++ .../server/controller/UserControllerTest.java | 34 ++++++++++++++ 6 files changed, 97 insertions(+), 12 deletions(-) diff --git a/client/src/__test__/context/AuthProvider.test.tsx b/client/src/__test__/context/AuthProvider.test.tsx index 7487b5fc..743ebacc 100644 --- a/client/src/__test__/context/AuthProvider.test.tsx +++ b/client/src/__test__/context/AuthProvider.test.tsx @@ -7,6 +7,7 @@ import AuthProvider from '../../context/AuthProvider'; import AuthContext, { AuthContextData } from '../../context/AuthContext'; import api from '../../api-service/api'; import { API_TOKEN, USER_DATA } from '../../app-constants/app-constants'; +import ApiConfig from '../../api-service/apiConfig'; // Mock the API service methods. vi.mock('../../api-service/api'); @@ -119,17 +120,29 @@ describe('AuthProvider', () => { }); it('should set loading to false and signed to true after successful initial auth check', async () => { - const fakeResponse = { + const fakeTokenResponse = { token: 'refresh-token', + }; + const fakeCurrentUser = { userId: '789', name: 'Refreshed User', email: 'refreshed@example.com', admin: false, createdAt: new Date().toISOString(), - gravatarImageUrl: 'http://dummyimage.com' + gravatarImageUrl: 'http://dummyimage.com', + lang: 'en', + lastLogin: new Date().toISOString() }; - vi.spyOn(api, 'getJSON').mockResolvedValue(fakeResponse); + vi.spyOn(api, 'getJSON').mockImplementation(async(url: string) => { + if (url === ApiConfig.refreshTokenUrl) { + return fakeTokenResponse; + } + if (url === ApiConfig.currentUserUrl) { + return fakeCurrentUser; + } + return undefined; + }); localStorage.setItem(API_TOKEN, 'dummy'); const { getByTestId } = render( @@ -142,6 +155,7 @@ describe('AuthProvider', () => { expect(getByTestId('loading').textContent).toBe('false') ); expect(getByTestId('signed').textContent).toBe('true'); + expect(getByTestId('user').textContent).toBe('Refreshed User'); }); it('should sign in a user successfully', async () => { @@ -251,17 +265,29 @@ describe('AuthProvider', () => { }); it('should call fetchCurrentSession when checking current auth user', async () => { - const fakeResponse = { + const fakeTokenResponse = { token: 'refresh-token', + }; + const fakeCurrentUser = { userId: '789', name: 'Refreshed User', email: 'refreshed@example.com', admin: false, createdAt: new Date().toISOString(), - gravatarImageUrl: 'http://dummyimage.com' + gravatarImageUrl: 'http://dummyimage.com', + lang: 'en', + lastLogin: new Date().toISOString() }; - vi.spyOn(api, 'getJSON').mockResolvedValue(fakeResponse); + vi.spyOn(api, 'getJSON').mockImplementation(async(url: string) => { + if (url === ApiConfig.refreshTokenUrl) { + return fakeTokenResponse; + } + if (url === ApiConfig.currentUserUrl) { + return fakeCurrentUser; + } + return undefined; + }); // Store API_TOKEN so that fetchCurrentSession runs the refresh logic. localStorage.setItem(API_TOKEN, 'dummy'); @@ -283,8 +309,9 @@ describe('AuthProvider', () => { await user.click(getByTestIdFunction('checkCurrentAuthUser')); - await waitFor(() => - expect(localStorage.getItem(API_TOKEN)).toBe('refresh-token') - ); + await waitFor(() => { + expect(localStorage.getItem(API_TOKEN)).toBe('refresh-token'); + expect(localStorage.getItem(USER_DATA)).toContain('Refreshed User'); + }); }); }); diff --git a/client/src/api-service/apiConfig.ts b/client/src/api-service/apiConfig.ts index 15a7d27e..2a99024f 100644 --- a/client/src/api-service/apiConfig.ts +++ b/client/src/api-service/apiConfig.ts @@ -28,7 +28,9 @@ const ApiConfig = { publicNotesUrl: `${server}/public/notes`, - userUrl: `${server}/rest/users` + userUrl: `${server}/rest/users`, + + currentUserUrl: `${server}/rest/users/me` }; export default ApiConfig; diff --git a/client/src/context/AuthProvider.tsx b/client/src/context/AuthProvider.tsx index 34391a93..e32309a7 100644 --- a/client/src/context/AuthProvider.tsx +++ b/client/src/context/AuthProvider.tsx @@ -24,7 +24,6 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro } try { const bearerToken: SignInResponse = await api.getJSON(ApiConfig.refreshTokenUrl); - setSigned(true); return bearerToken; } catch (e) { @@ -66,8 +65,10 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro const checkCurrentAuthUser = async (pathname: string): Promise => { const bearerToken: SignInResponse | undefined = await fetchCurrentSession(pathname); if (bearerToken && bearerToken.token) { - const userLocal = updateUserSession(null, bearerToken.token); + const currentUser: UserResponse = await api.getJSON(ApiConfig.currentUserUrl); + const userLocal = updateUserSession(currentUser, bearerToken.token); if (userLocal) { + setSigned(true); setUser(userLocal); } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/UserController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/UserController.java index fa0d60f0..7b848828 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/UserController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/UserController.java @@ -33,6 +33,16 @@ public List getAllUsers() { return authService.getAllUsers(); } + /** + * Get the current logged user. + * + * @return UserEntity with the current user information. + */ + @GetMapping("/me") + public UserResponse getCurrentUser() { + return authService.getCurrentUserResponse(); + } + @PatchMapping public ResponseEntity patchUserInfo( @RequestBody @Valid UserPatchRequest taskRequest) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java index 0ffa5c0d..5bcb0315 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java @@ -393,6 +393,17 @@ public Optional getCurrentUser() { return findByEmail(email); } + /** + * Get the current logged user as response object. + * + * @return An instance of {@link UserResponse} with the current user. + * @throws UserNotFoundException when the user was not found + */ + public UserResponse getCurrentUserResponse() { + UserEntity user = getCurrentUser().orElseThrow(UserNotFoundException::new); + return UserResponse.fromEntity(user, getGravatarImageUrl(user.getEmail()).orElse(null)); + } + /** * Confirm a user account. * diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java index 31c43a32..42603f35 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java @@ -63,6 +63,40 @@ void getAllUsers_unauthorized_shouldFail() throws Exception { .andReturn(); } + @Test + @DisplayName("Get current user happy path should succeed") + @WithMockUser(username = "user@domain.com", password = "abcde123456A@") + void getCurrentUser_happyPath_shouldSucceed() throws Exception { + UserResponse userResponse = + new UserResponse(1L, "John", "email@test.com", false, null, null, null, null); + when(authService.getCurrentUserResponse()).thenReturn(userResponse); + + mockMvc + .perform( + get("/rest/users/me") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(userResponse.userId())) + .andExpect(jsonPath("$.email").value(userResponse.email())) + .andExpect(jsonPath("$.admin").value(userResponse.admin())) + .andReturn(); + } + + @Test + @DisplayName("Get current user with 401 unauthorized request should fail") + void getCurrentUser_unauthorized_shouldFail() throws Exception { + mockMvc + .perform( + get("/rest/users/me") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()) + .andReturn(); + } + @Test @DisplayName("Patch user info happy path should succeed") @WithMockUser(username = "user@domain.com", password = "abcde123456A@") From 469626c02edc1ea3e8e0891d1293a6ba0919e6f9 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Mon, 18 May 2026 22:15:53 -0300 Subject: [PATCH 3/3] test: fix time zone test issue --- .../tasknoteapp/server/util/TimeAgoUtil.java | 22 ++++++++++++++----- .../server/util/TimeAgoUtilTest.java | 15 +++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java b/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java index dcd5a552..a27fa318 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java +++ b/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java @@ -18,10 +18,20 @@ public class TimeAgoUtil { * @return String with formatted time. */ public static String format(LocalDateTime pastTime) { + return format(pastTime, LocalDateTime.now()); + } + + /** + * Format a time in the time ago format. + * + * @param pastTime The pastime to be formatted. + * @param now The current time. + * @return String with formatted time. + */ + public static String format(LocalDateTime pastTime, LocalDateTime now) { if (Objects.isNull(pastTime)) { return "Some time ago"; } - LocalDateTime now = LocalDateTime.now(); Period period = Period.between(pastTime.toLocalDate(), now.toLocalDate()); Duration duration = Duration.between(pastTime, now); if (period.getYears() > 1) { @@ -32,10 +42,12 @@ public static String format(LocalDateTime pastTime) { return String.format("%d months ago", period.getMonths()); } else if (period.getMonths() > 0) { return String.format("%d month ago", period.getMonths()); - } else if (period.getDays() > 1) { - return String.format("%d days ago", period.getDays()); - } else if (period.getDays() > 0) { - return String.format("%d day ago", period.getDays()); + } else if (duration.toHours() >= 24) { + if (period.getDays() > 1) { + return String.format("%d days ago", period.getDays()); + } else { + return String.format("%d day ago", period.getDays()); + } } else if (duration.toHours() > 1L) { return String.format("%d hours ago", duration.toHours()); } else if (duration.toHours() > 0L) { diff --git a/server/src/test/java/br/com/tasknoteapp/server/util/TimeAgoUtilTest.java b/server/src/test/java/br/com/tasknoteapp/server/util/TimeAgoUtilTest.java index c830f108..83a8ce69 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/util/TimeAgoUtilTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/util/TimeAgoUtilTest.java @@ -21,6 +21,21 @@ void formatTest() { "2 months ago", TimeAgoUtil.format(LocalDateTime.now().minusMonths(2L))); } + @Test + void formatMidnightRolloverTest() { + LocalDateTime now = LocalDateTime.of(2026, 5, 19, 0, 30); // 12:30 AM next day + LocalDateTime pastTime = now.minusHours(1); // 11:30 PM previous day + Assertions.assertEquals("1 hour ago", TimeAgoUtil.format(pastTime, now)); + + // Test exactly 24 hours ago + LocalDateTime twentyFourHoursAgo = now.minusDays(1); + Assertions.assertEquals("1 day ago", TimeAgoUtil.format(twentyFourHoursAgo, now)); + + // Test 23 hours ago across midnight + LocalDateTime twentyThreeHoursAgo = now.minusHours(23); // 1:30 AM previous day + Assertions.assertEquals("23 hours ago", TimeAgoUtil.format(twentyThreeHoursAgo, now)); + } + @Test void formatDueDateTest() { Assertions.assertNull(TimeAgoUtil.formatDueDate(null));