From 453e6143831834cf9f0151174aec92ded9cf5830 Mon Sep 17 00:00:00 2001 From: Ashley Clifton Date: Mon, 10 Oct 2022 13:58:20 -0400 Subject: [PATCH] Move PasswordResetToken endpoints into their own folder and controller (#483) --- .../retroquest/email/EmailController.java | 4 +- .../labs/retroquest/email/EmailService.java | 2 +- .../PasswordResetToken.java | 20 ++- .../PasswordResetTokenController.java | 52 ++++++++ .../PasswordResetTokenRepository.java | 4 +- .../PasswordResetTokenService.java | 2 +- .../labs/retroquest/team/TeamController.java | 16 +-- .../labs/retroquest/api/EmailApiTest.java | 4 +- .../api/PasswordResetTokenApiTest.java | 122 ++++++++++++++++++ .../ford/labs/retroquest/api/TeamApiTest.java | 75 +---------- .../retroquest/email/EmailServiceTest.java | 2 +- .../ExpiredLinkPage/ExpiredLinkPage.spec.tsx | 9 +- .../App/ExpiredLinkPage/ExpiredLinkPage.tsx | 4 +- .../ResetPassword/ResetPasswordPage.spec.tsx | 24 ++-- .../App/ResetPassword/ResetPasswordPage.tsx | 3 +- ui/src/Services/Api/ApiConstants.ts | 3 - .../Services/Api/ConfigurationService.spec.ts | 16 +++ .../Api/PasswordResetTokenService.spec.ts | 57 ++++++++ .../Services/Api/PasswordResetTokenService.ts | 34 +++++ ui/src/Services/Api/TeamService.spec.ts | 32 ----- ui/src/Services/Api/TeamService.ts | 16 --- .../__mocks__/PasswordResetTokenService.ts | 20 +-- ui/src/Services/Api/__mocks__/TeamService.ts | 2 - 23 files changed, 341 insertions(+), 182 deletions(-) rename api/src/main/java/com/ford/labs/retroquest/{team/password => password_reset_token}/PasswordResetToken.java (58%) create mode 100644 api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenController.java rename api/src/main/java/com/ford/labs/retroquest/{team/password => password_reset_token}/PasswordResetTokenRepository.java (91%) rename api/src/main/java/com/ford/labs/retroquest/{team/password => password_reset_token}/PasswordResetTokenService.java (96%) create mode 100644 api/src/test/java/com/ford/labs/retroquest/api/PasswordResetTokenApiTest.java create mode 100644 ui/src/Services/Api/PasswordResetTokenService.spec.ts create mode 100644 ui/src/Services/Api/PasswordResetTokenService.ts rename api/src/main/java/com/ford/labs/retroquest/team/ResetTokenStatusRequest.java => ui/src/Services/Api/__mocks__/PasswordResetTokenService.ts (63%) diff --git a/api/src/main/java/com/ford/labs/retroquest/email/EmailController.java b/api/src/main/java/com/ford/labs/retroquest/email/EmailController.java index 82105af8a..98c17de8f 100644 --- a/api/src/main/java/com/ford/labs/retroquest/email/EmailController.java +++ b/api/src/main/java/com/ford/labs/retroquest/email/EmailController.java @@ -19,10 +19,10 @@ import com.ford.labs.retroquest.exception.EmailNotAssociatedWithAnyTeamsException; import com.ford.labs.retroquest.exception.TeamDoesNotExistException; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetTokenService; import com.ford.labs.retroquest.team.Team; import com.ford.labs.retroquest.team.TeamService; -import com.ford.labs.retroquest.team.password.PasswordResetToken; -import com.ford.labs.retroquest.team.password.PasswordResetTokenService; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.web.bind.annotation.PostMapping; diff --git a/api/src/main/java/com/ford/labs/retroquest/email/EmailService.java b/api/src/main/java/com/ford/labs/retroquest/email/EmailService.java index de7728747..0fb47f8f7 100644 --- a/api/src/main/java/com/ford/labs/retroquest/email/EmailService.java +++ b/api/src/main/java/com/ford/labs/retroquest/email/EmailService.java @@ -16,7 +16,7 @@ */ package com.ford.labs.retroquest.email; -import com.ford.labs.retroquest.team.password.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.SimpleMailMessage; diff --git a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetToken.java b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetToken.java similarity index 58% rename from api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetToken.java rename to api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetToken.java index 8478f13c9..7c2423556 100644 --- a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetToken.java +++ b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetToken.java @@ -1,4 +1,20 @@ -package com.ford.labs.retroquest.team.password; +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ford.labs.retroquest.password_reset_token; import com.fasterxml.jackson.annotation.JsonIgnore; import com.ford.labs.retroquest.team.Team; @@ -6,11 +22,9 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import javax.persistence.*; import java.time.Duration; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; diff --git a/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenController.java b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenController.java new file mode 100644 index 000000000..1f1f7d289 --- /dev/null +++ b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenController.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ford.labs.retroquest.password_reset_token; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/password-reset-token") +@Tag(name = "Password Reset Token Controller", description = "The controller that manages password reset tokens") +public class PasswordResetTokenController { + + private final PasswordResetTokenRepository passwordResetRepository; + + public PasswordResetTokenController(PasswordResetTokenRepository passwordResetRepository) { + this.passwordResetRepository = passwordResetRepository; + } + + @GetMapping("/lifetime-in-seconds") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + @Operation(description = "Get the number of seconds before a password reset token expires") + public ResponseEntity getResetTokenValiditySeconds(@Value("${retroquest.password.reset.token-lifetime-seconds:600}") int tokenSeconds){ + return ResponseEntity.ok(tokenSeconds); + } + + @GetMapping("/{passwordResetToken}/is-valid") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + @Operation(description = "Check if the reset token is valid") + public boolean checkResetTokenStatus(@PathVariable("passwordResetToken") String passwordResetToken) { + PasswordResetToken token = passwordResetRepository.findByResetToken(passwordResetToken); + return token != null && !token.isExpired(); + } +} diff --git a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenRepository.java b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenRepository.java similarity index 91% rename from api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenRepository.java rename to api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenRepository.java index ba45008d6..0710e22fc 100644 --- a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenRepository.java +++ b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenRepository.java @@ -15,15 +15,13 @@ * limitations under the License. */ -package com.ford.labs.retroquest.team.password; +package com.ford.labs.retroquest.password_reset_token; import com.ford.labs.retroquest.team.Team; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import javax.transaction.Transactional; -import java.time.LocalDate; -import java.util.Optional; @Repository public interface PasswordResetTokenRepository extends JpaRepository { diff --git a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenService.java b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenService.java similarity index 96% rename from api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenService.java rename to api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenService.java index 84dac1f10..701f2a409 100644 --- a/api/src/main/java/com/ford/labs/retroquest/team/password/PasswordResetTokenService.java +++ b/api/src/main/java/com/ford/labs/retroquest/password_reset_token/PasswordResetTokenService.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ford.labs.retroquest.team.password; +package com.ford.labs.retroquest.password_reset_token; import com.ford.labs.retroquest.team.Team; import org.springframework.stereotype.Service; diff --git a/api/src/main/java/com/ford/labs/retroquest/team/TeamController.java b/api/src/main/java/com/ford/labs/retroquest/team/TeamController.java index 297c488e1..34bf4f523 100644 --- a/api/src/main/java/com/ford/labs/retroquest/team/TeamController.java +++ b/api/src/main/java/com/ford/labs/retroquest/team/TeamController.java @@ -19,8 +19,8 @@ import com.ford.labs.retroquest.exception.BadResetTokenException; import com.ford.labs.retroquest.security.JwtBuilder; -import com.ford.labs.retroquest.team.password.PasswordResetToken; -import com.ford.labs.retroquest.team.password.PasswordResetTokenRepository; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetTokenRepository; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -110,18 +110,6 @@ public void resetPassword(@RequestBody ResetPasswordRequest resetPasswordRequest passwordResetRepository.delete(passwordResetToken); } - @PostMapping("/password/reset/is-valid") - public boolean checkResetTokenStatus(@RequestBody ResetTokenStatusRequest resetTokenStatusRequest) { - PasswordResetToken passwordResetToken = passwordResetRepository.findByResetToken(resetTokenStatusRequest.getResetToken()); - return passwordResetToken != null && !passwordResetToken.isExpired(); - } - - @GetMapping("/password/reset/token-lifetime-seconds") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) - public ResponseEntity getResetTokenValiditySeconds(@Value("${retroquest.password.reset.token-lifetime-seconds:600}") int tokenSeconds){ - return ResponseEntity.ok(tokenSeconds); - } - @GetMapping(value = "/team/{teamId}/csv", produces = "application/board.csv") @PreAuthorize("@teamAuthorization.requestIsAuthorized(authentication, #teamId)") @Operation(summary = "downloads a team board", description = "downloadTeamBoard") diff --git a/api/src/test/java/com/ford/labs/retroquest/api/EmailApiTest.java b/api/src/test/java/com/ford/labs/retroquest/api/EmailApiTest.java index e1ffca7f4..9d017f07f 100644 --- a/api/src/test/java/com/ford/labs/retroquest/api/EmailApiTest.java +++ b/api/src/test/java/com/ford/labs/retroquest/api/EmailApiTest.java @@ -20,10 +20,10 @@ import com.ford.labs.retroquest.email.EmailService; import com.ford.labs.retroquest.email.RecoverTeamNamesRequest; import com.ford.labs.retroquest.email.RequestPasswordResetRequest; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetTokenRepository; import com.ford.labs.retroquest.team.Team; import com.ford.labs.retroquest.team.TeamRepository; -import com.ford.labs.retroquest.team.password.PasswordResetToken; -import com.ford.labs.retroquest.team.password.PasswordResetTokenRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; diff --git a/api/src/test/java/com/ford/labs/retroquest/api/PasswordResetTokenApiTest.java b/api/src/test/java/com/ford/labs/retroquest/api/PasswordResetTokenApiTest.java new file mode 100644 index 000000000..a793a01a0 --- /dev/null +++ b/api/src/test/java/com/ford/labs/retroquest/api/PasswordResetTokenApiTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ford.labs.retroquest.api; + +import com.ford.labs.retroquest.api.setup.ApiTestBase; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetTokenRepository; +import com.ford.labs.retroquest.team.Team; +import com.ford.labs.retroquest.team.TeamRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.MvcResult; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Tag("api") +class PasswordResetTokenApiTest extends ApiTestBase { + + @Autowired + private PasswordResetTokenRepository passwordResetRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + PasswordEncoder passwordEncoder; + + private final String passwordResetTokenRootPath = "/api/password-reset-token"; + + private Team team; + + @BeforeEach + void beforeClass() { + clean(); + team = new Team("teamuri", "TeamName", "%$&357", "e@ma.il"); + teamRepository.save(team); + } + + @AfterEach + void clean() { + passwordResetRepository.deleteAllInBatch(); + teamRepository.deleteAllInBatch(); + assertThat(passwordResetRepository.count()).isZero(); + assertThat(teamRepository.count()).isZero(); + } + + @Test + public void get_reset_token_validity_seconds__shouldReportCorrectPasswordTokenExpirationTime() throws Exception { + String tokenLifeTimePath = passwordResetTokenRootPath + "/lifetime-in-seconds"; + MvcResult mvcResult = mockMvc.perform(get(tokenLifeTimePath)).andExpect(status().isOk()).andReturn(); + assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("600"); + } + + @Test + void check_reset_token_status__should_return_true_when_password_reset_token_is_valid() throws Exception { + PasswordResetToken passwordResetToken = new PasswordResetToken(); + passwordResetToken.setTeam(team); + passwordResetRepository.save(passwordResetToken); + + var mvcResult = mockMvc.perform(get(getIsResetTokenValidPath(passwordResetToken))) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("true"); + } + + @Test + void check_reset_token_status__should_return_false_when_password_reset_token_is_expired() throws Exception { + PasswordResetToken passwordResetToken = new PasswordResetToken(); + passwordResetToken.setDateCreated(LocalDateTime.MIN); + passwordResetToken.setTeam(team); + passwordResetRepository.save(passwordResetToken); + + var mvcResult = mockMvc.perform(get(getIsResetTokenValidPath(passwordResetToken))) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("false"); + } + + @Test + void check_reset_token_status__should_return_false_when_password_reset_token_does_not_exist() throws Exception { + PasswordResetToken passwordResetToken = new PasswordResetToken(); + passwordResetToken.setDateCreated(LocalDateTime.MIN); + passwordResetToken.setTeam(team); + passwordResetRepository.save(passwordResetToken); + + var mvcResult = mockMvc.perform(get(getIsResetTokenValidPath(passwordResetToken))) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("false"); + } + + private String getIsResetTokenValidPath(PasswordResetToken passwordResetToken) { + return passwordResetTokenRootPath + "/" + passwordResetToken.getResetToken() + "/is-valid"; + } +} diff --git a/api/src/test/java/com/ford/labs/retroquest/api/TeamApiTest.java b/api/src/test/java/com/ford/labs/retroquest/api/TeamApiTest.java index fcb228f45..c99a7511d 100644 --- a/api/src/test/java/com/ford/labs/retroquest/api/TeamApiTest.java +++ b/api/src/test/java/com/ford/labs/retroquest/api/TeamApiTest.java @@ -20,8 +20,8 @@ import com.ford.labs.retroquest.api.setup.ApiTestBase; import com.ford.labs.retroquest.column.ColumnRepository; import com.ford.labs.retroquest.team.*; -import com.ford.labs.retroquest.team.password.PasswordResetToken; -import com.ford.labs.retroquest.team.password.PasswordResetTokenRepository; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetTokenRepository; import io.micrometer.core.instrument.MeterRegistry; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; @@ -210,77 +210,6 @@ void should_create_team_with_valid_name_and_password_and_two_emails() throws Exc assertThat(mvcResult.getResponse().getContentAsString()).isNotNull(); } - @Test - public void shouldReportCorrectPasswordTokenExpirationTime() throws Exception { - MvcResult mvcResult = mockMvc.perform(get("/api/password/reset/token-lifetime-seconds")).andExpect(status().isOk()).andReturn(); - assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("600"); - } - - @Test - void should_return_true_when_password_reset_token_is_valid() throws Exception { - Team team = new Team("teamuri", "TeamName", "%$&357", "e@ma.il"); - teamRepository.save(team); - PasswordResetToken passwordResetToken = new PasswordResetToken(); - passwordResetToken.setTeam(team); - passwordResetRepository.save(passwordResetToken); - - var mvcResult = mockMvc.perform( - post("/api/password/reset/is-valid") - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes( - new ResetTokenStatusRequest(passwordResetToken.getResetToken())) - ) - ) - .andExpect(status().is2xxSuccessful()) - .andReturn(); - - assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("true"); - } - - @Test - void should_return_false_when_password_reset_token_is_expired() throws Exception { - Team team = new Team("teamuri", "TeamName", "%$&357", "e@ma.il"); - teamRepository.save(team); - PasswordResetToken passwordResetToken = new PasswordResetToken(); - passwordResetToken.setDateCreated(LocalDateTime.MIN); - passwordResetToken.setTeam(team); - passwordResetRepository.save(passwordResetToken); - - var mvcResult = mockMvc.perform( - post("/api/password/reset/is-valid") - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes( - new ResetTokenStatusRequest(passwordResetToken.getResetToken())) - ) - ) - .andExpect(status().is2xxSuccessful()) - .andReturn(); - - assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("false"); - } - - @Test - void should_return_false_when_password_reset_token_does_not_exist() throws Exception { - Team team = new Team("teamuri", "TeamName", "%$&357", "e@ma.il"); - teamRepository.save(team); - PasswordResetToken passwordResetToken = new PasswordResetToken(); - passwordResetToken.setDateCreated(LocalDateTime.MIN); - passwordResetToken.setTeam(team); - passwordResetRepository.save(passwordResetToken); - - var mvcResult = mockMvc.perform( - post("/api/password/reset/is-valid") - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes( - new ResetTokenStatusRequest(UUID.randomUUID().toString())) - ) - ) - .andExpect(status().is2xxSuccessful()) - .andReturn(); - - assertThat(mvcResult.getResponse().getContentAsString()).isEqualTo("false"); - } - @Test void should_change_password_and_consume_token_when_reset_token_is_valid() throws Exception { Team expectedResetTeam = new Team("teamuri", "TeamName", "%$&357", "e@ma.il"); diff --git a/api/src/test/java/com/ford/labs/retroquest/email/EmailServiceTest.java b/api/src/test/java/com/ford/labs/retroquest/email/EmailServiceTest.java index 22ea2894e..fd8c2aa3f 100644 --- a/api/src/test/java/com/ford/labs/retroquest/email/EmailServiceTest.java +++ b/api/src/test/java/com/ford/labs/retroquest/email/EmailServiceTest.java @@ -18,7 +18,7 @@ package com.ford.labs.retroquest.email; import com.ford.labs.retroquest.team.Team; -import com.ford.labs.retroquest.team.password.PasswordResetToken; +import com.ford.labs.retroquest.password_reset_token.PasswordResetToken; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; diff --git a/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.spec.tsx b/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.spec.tsx index b08d0b5e9..cc93a557c 100644 --- a/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.spec.tsx +++ b/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.spec.tsx @@ -19,7 +19,7 @@ import { MemoryRouter } from 'react-router-dom'; import { screen, waitFor } from '@testing-library/react'; import { MutableSnapshot } from 'recoil'; import ContributorsService from 'Services/Api/ContributorsService'; -import TeamService from 'Services/Api/TeamService'; +import PasswordResetTokenService from 'Services/Api/PasswordResetTokenService'; import { ThemeState } from 'State/ThemeState'; import Theme from 'Types/Theme'; import renderWithRecoilRoot from 'Utils/renderWithRecoilRoot'; @@ -28,6 +28,7 @@ import ExpiredLinkPage from './ExpiredLinkPage'; jest.mock('Services/Api/ContributorsService'); jest.mock('Services/Api/TeamService'); +jest.mock('Services/Api/PasswordResetTokenService'); describe('Expired Link Page', () => { it('should render header and link to reset password page', async () => { @@ -42,10 +43,12 @@ describe('Expired Link Page', () => { }); it('should show token lifetime in page description', async () => { - TeamService.getResetTokenLifetime = jest.fn().mockResolvedValue(600); + PasswordResetTokenService.getResetTokenLifetime = jest + .fn() + .mockResolvedValue(600); await renderExpiredLinkPage(); await waitFor(() => - expect(TeamService.getResetTokenLifetime).toHaveBeenCalled() + expect(PasswordResetTokenService.getResetTokenLifetime).toHaveBeenCalled() ); expect( screen.getByText( diff --git a/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.tsx b/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.tsx index 6d6ceaab8..cb97bcf31 100644 --- a/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.tsx +++ b/ui/src/App/ExpiredLinkPage/ExpiredLinkPage.tsx @@ -21,7 +21,7 @@ import AuthTemplate from 'Common/AuthTemplate/AuthTemplate'; import LinkPrimary from 'Common/LinkPrimary/LinkPrimary'; import { useRecoilValue } from 'recoil'; import { PASSWORD_RESET_REQUEST_PATH } from 'RouteConstants'; -import TeamService from 'Services/Api/TeamService'; +import PasswordResetTokenService from 'Services/Api/PasswordResetTokenService'; import { ThemeState } from 'State/ThemeState'; import Theme from 'Types/Theme'; @@ -34,7 +34,7 @@ function ExpiredLinkPage() { const [resetTokenLifetime, setResetTokenLifetime] = useState(600); useEffect(() => { - TeamService.getResetTokenLifetime().then((seconds) => { + PasswordResetTokenService.getResetTokenLifetime().then((seconds) => { setResetTokenLifetime(Math.floor(seconds / 60)); }); }, []); diff --git a/ui/src/App/ResetPassword/ResetPasswordPage.spec.tsx b/ui/src/App/ResetPassword/ResetPasswordPage.spec.tsx index c4432f557..65ac3da29 100644 --- a/ui/src/App/ResetPassword/ResetPasswordPage.spec.tsx +++ b/ui/src/App/ResetPassword/ResetPasswordPage.spec.tsx @@ -17,12 +17,14 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { fireEvent, screen, waitFor } from '@testing-library/react'; +import PasswordResetTokenService from 'Services/Api/PasswordResetTokenService'; import TeamService from 'Services/Api/TeamService'; import renderWithRecoilRoot from 'Utils/renderWithRecoilRoot'; import ResetPasswordPage from './ResetPasswordPage'; jest.mock('Services/Api/TeamService'); +jest.mock('Services/Api/PasswordResetTokenService'); const mockedUsedNavigate = jest.fn(); @@ -32,7 +34,7 @@ jest.mock('react-router-dom', () => ({ })); describe('Reset Password Page', () => { - it('should have retroquest header', () => { + it('should have RetroQuest header', () => { renderWithToken(''); expect(screen.getByText('RetroQuest')).toBeDefined(); }); @@ -48,26 +50,30 @@ describe('Reset Password Page', () => { describe('Token Validity', () => { it('should check if token is valid and navigate to Expired Token page if token is invalid', async () => { - TeamService.checkIfResetTokenIsValid = jest.fn().mockResolvedValue(false); + PasswordResetTokenService.checkIfResetTokenIsValid = jest + .fn() + .mockResolvedValue(false); renderWithToken('expired-token'); await waitFor(() => - expect(TeamService.checkIfResetTokenIsValid).toHaveBeenCalledWith( - 'expired-token' - ) + expect( + PasswordResetTokenService.checkIfResetTokenIsValid + ).toHaveBeenCalledWith('expired-token') ); expect(mockedUsedNavigate).toHaveBeenCalledWith('/expired-link'); expect(screen.queryByText('Reset Your Password')).toBeInTheDocument(); }); it('should check if token is valid and stay on page if it is valid', async () => { - TeamService.checkIfResetTokenIsValid = jest.fn().mockResolvedValue(true); + PasswordResetTokenService.checkIfResetTokenIsValid = jest + .fn() + .mockResolvedValue(true); renderWithToken('valid-token'); await waitFor(() => - expect(TeamService.checkIfResetTokenIsValid).toHaveBeenCalledWith( - 'valid-token' - ) + expect( + PasswordResetTokenService.checkIfResetTokenIsValid + ).toHaveBeenCalledWith('valid-token') ); expect(mockedUsedNavigate).not.toHaveBeenCalled(); expect(screen.getByText('Reset Your Password')).toBeInTheDocument(); diff --git a/ui/src/App/ResetPassword/ResetPasswordPage.tsx b/ui/src/App/ResetPassword/ResetPasswordPage.tsx index 374281699..9ce9987c5 100644 --- a/ui/src/App/ResetPassword/ResetPasswordPage.tsx +++ b/ui/src/App/ResetPassword/ResetPasswordPage.tsx @@ -22,6 +22,7 @@ import Header from 'Common/Header/Header'; import InputPassword from 'Common/InputPassword/InputPassword'; import LinkPrimary from 'Common/LinkPrimary/LinkPrimary'; import LinkTertiary from 'Common/LinkTertiary/LinkTertiary'; +import PasswordResetTokenService from 'Services/Api/PasswordResetTokenService'; import TeamService from 'Services/Api/TeamService'; import { EXPIRED_LINK_PATH, LOGIN_PAGE_PATH } from '../../RouteConstants'; @@ -46,7 +47,7 @@ function ResetPasswordPage(): JSX.Element { } useEffect(() => { - TeamService.checkIfResetTokenIsValid(passwordResetToken).then( + PasswordResetTokenService.checkIfResetTokenIsValid(passwordResetToken).then( (isValidToken) => { if (!isValidToken) navigate(EXPIRED_LINK_PATH); } diff --git a/ui/src/Services/Api/ApiConstants.ts b/ui/src/Services/Api/ApiConstants.ts index 9c2cdc590..e3a18cbff 100644 --- a/ui/src/Services/Api/ApiConstants.ts +++ b/ui/src/Services/Api/ApiConstants.ts @@ -18,9 +18,6 @@ export const TEAM_API_PATH = '/api/team'; export const CHANGE_EMAIL_API_PATH = '/api/email/reset'; export const CHANGE_PASSWORD_API_PATH = '/api/password/reset'; -export const RESET_TOKEN_LIFETIME_API_PATH = - '/api/password/reset/token-lifetime-seconds'; -export const RESET_TOKEN_STATUS_API_PATH = '/api/password/reset/is-valid'; export const EMAIL_PASSWORD_REQUEST_API_PATH = '/api/email/password-reset-request'; export const EMAIL_TEAM_NAME_RECOVERY_API_PATH = diff --git a/ui/src/Services/Api/ConfigurationService.spec.ts b/ui/src/Services/Api/ConfigurationService.spec.ts index ee9c544f5..4ec4ac129 100644 --- a/ui/src/Services/Api/ConfigurationService.spec.ts +++ b/ui/src/Services/Api/ConfigurationService.spec.ts @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import axios from 'axios'; import configurationService from './ConfigurationService'; diff --git a/ui/src/Services/Api/PasswordResetTokenService.spec.ts b/ui/src/Services/Api/PasswordResetTokenService.spec.ts new file mode 100644 index 000000000..b5ec0e17e --- /dev/null +++ b/ui/src/Services/Api/PasswordResetTokenService.spec.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import axios from 'axios'; + +import PasswordResetTokenService from './PasswordResetTokenService'; + +describe('Password Reset Token Service', () => { + describe('checkIfResetTokenIsValid', () => { + it('should return true when token is valid', async () => { + axios.get = jest.fn().mockResolvedValue({ data: true }); + const isValidToken = + await PasswordResetTokenService.checkIfResetTokenIsValid('valid-token'); + expect(axios.get).toHaveBeenCalledWith( + '/api/password-reset-token/valid-token/is-valid' + ); + expect(isValidToken).toBeTruthy(); + }); + + it('should return true when token is invalid', async () => { + axios.get = jest.fn().mockResolvedValue({ data: false }); + const isValidToken = + await PasswordResetTokenService.checkIfResetTokenIsValid( + 'invalid-token' + ); + expect(axios.get).toHaveBeenCalledWith( + '/api/password-reset-token/invalid-token/is-valid' + ); + expect(isValidToken).toBeFalsy(); + }); + }); + + describe('getResetTokenLifetime', () => { + it('should get the lifetime of the reset token', async () => { + axios.get = jest.fn().mockResolvedValue({ data: 500 }); + const actualTokenLifetime = + await PasswordResetTokenService.getResetTokenLifetime(); + expect(axios.get).toHaveBeenCalledWith( + '/api/password-reset-token/lifetime-in-seconds' + ); + expect(actualTokenLifetime).toBe(500); + }); + }); +}); diff --git a/ui/src/Services/Api/PasswordResetTokenService.ts b/ui/src/Services/Api/PasswordResetTokenService.ts new file mode 100644 index 000000000..de865aebd --- /dev/null +++ b/ui/src/Services/Api/PasswordResetTokenService.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Ford Motor Company + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import axios from 'axios'; + +const PASSWORD_RESET_TOKEN_API_PATH = '/api/password-reset-token'; + +const PasswordResetTokenService = { + checkIfResetTokenIsValid(resetToken: string): Promise { + const url = `${PASSWORD_RESET_TOKEN_API_PATH}/${resetToken}/is-valid`; + return axios.get(url).then((res) => res.data); + }, + + getResetTokenLifetime(): Promise { + return axios + .get(`${PASSWORD_RESET_TOKEN_API_PATH}/lifetime-in-seconds`) + .then((response) => response.data); + }, +}; + +export default PasswordResetTokenService; diff --git a/ui/src/Services/Api/TeamService.spec.ts b/ui/src/Services/Api/TeamService.spec.ts index 50ffc78bc..7f2529bda 100644 --- a/ui/src/Services/Api/TeamService.spec.ts +++ b/ui/src/Services/Api/TeamService.spec.ts @@ -169,38 +169,6 @@ describe('Team Service', () => { }); }); - describe('checkIfResetTokenIsValid', () => { - it('should return true when token is valid', async () => { - axios.post = jest.fn().mockResolvedValue({ data: true }); - const isValidToken = await TeamService.checkIfResetTokenIsValid( - 'valid-token' - ); - expect(axios.post).toHaveBeenCalledWith('/api/password/reset/is-valid', { - resetToken: 'valid-token', - }); - expect(isValidToken).toBeTruthy(); - }); - - it('should return true when token is invalid', async () => { - axios.post = jest.fn().mockResolvedValue({ data: false }); - const isValidToken = await TeamService.checkIfResetTokenIsValid( - 'invalid-token' - ); - expect(axios.post).toHaveBeenCalledWith('/api/password/reset/is-valid', { - resetToken: 'invalid-token', - }); - expect(isValidToken).toBeFalsy(); - }); - }); - - describe('getResetTokenLifetime', () => { - it('should get the lifetime of the reset token', async () => { - axios.get = jest.fn().mockResolvedValue({ data: 500 }); - const actualTokenLifetime = await TeamService.getResetTokenLifetime(); - expect(actualTokenLifetime).toBe(500); - }); - }); - describe('onResponseInterceptRejection', () => { let location: (string | Location) & Location; let mockAssign = jest.fn(); diff --git a/ui/src/Services/Api/TeamService.ts b/ui/src/Services/Api/TeamService.ts index bdb42de3b..5b9e1984c 100644 --- a/ui/src/Services/Api/TeamService.ts +++ b/ui/src/Services/Api/TeamService.ts @@ -31,8 +31,6 @@ import { getCSVApiPath, getTeamNameApiPath, LOGIN_API_PATH, - RESET_TOKEN_LIFETIME_API_PATH, - RESET_TOKEN_STATUS_API_PATH, TEAM_API_PATH, } from './ApiConstants'; import getAuthConfig from './getAuthConfig'; @@ -85,14 +83,6 @@ const TeamService = { }); }, - checkIfResetTokenIsValid(resetToken: string): Promise { - return axios - .post(RESET_TOKEN_STATUS_API_PATH, { - resetToken: resetToken, - }) - .then((res) => res.data); - }, - setPassword(password: string, token: string): Promise { return axios.post(CHANGE_PASSWORD_API_PATH, { password: password, @@ -133,12 +123,6 @@ const TeamService = { .then((response) => response.data); }, - getResetTokenLifetime(): Promise { - return axios - .get(RESET_TOKEN_LIFETIME_API_PATH) - .then((response) => response.data); - }, - onResponseInterceptRejection(error: AxiosError): Promise { const { status } = error?.response || {}; diff --git a/api/src/main/java/com/ford/labs/retroquest/team/ResetTokenStatusRequest.java b/ui/src/Services/Api/__mocks__/PasswordResetTokenService.ts similarity index 63% rename from api/src/main/java/com/ford/labs/retroquest/team/ResetTokenStatusRequest.java rename to ui/src/Services/Api/__mocks__/PasswordResetTokenService.ts index 02e7b6e1f..58ee1c588 100644 --- a/api/src/main/java/com/ford/labs/retroquest/team/ResetTokenStatusRequest.java +++ b/ui/src/Services/Api/__mocks__/PasswordResetTokenService.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Ford Motor Company + * Copyright (c) 2022 Ford Motor Company * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,17 +15,9 @@ * limitations under the License. */ -package com.ford.labs.retroquest.team; +const PasswordResetTokenService = { + checkIfResetTokenIsValid: jest.fn().mockResolvedValue(true), + getResetTokenLifetime: jest.fn().mockResolvedValue(800), +}; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder(toBuilder=true) -public class ResetTokenStatusRequest { - private String resetToken; -} +export default PasswordResetTokenService; diff --git a/ui/src/Services/Api/__mocks__/TeamService.ts b/ui/src/Services/Api/__mocks__/TeamService.ts index 26cdeb0db..377c9a5a3 100644 --- a/ui/src/Services/Api/__mocks__/TeamService.ts +++ b/ui/src/Services/Api/__mocks__/TeamService.ts @@ -32,8 +32,6 @@ const TeamService = { getCSV: jest.fn().mockResolvedValue('column 1, column 2'), updateEmailsWithResetToken: jest.fn().mockResolvedValue(''), setPassword: jest.fn().mockResolvedValue(''), - checkIfResetTokenIsValid: jest.fn().mockResolvedValue(true), - getResetTokenLifetime: jest.fn().mockResolvedValue(800), updateTeamEmailAddresses: jest.fn().mockResolvedValue(''), };