Skip to content

Commit

Permalink
Added link controller test
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanShalaev1990 committed Apr 20, 2024
1 parent 63f6470 commit 8d392ae
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 24 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.3.0'
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.3.0'
testImplementation 'org.testcontainers:postgresql:1.19.7'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.linkurlshorter.urlshortener.exception;

import com.linkurlshorter.urlshortener.auth.exception.EmailAlreadyTakenException;
import com.linkurlshorter.urlshortener.link.ForbiddenException;
import com.linkurlshorter.urlshortener.link.NoLinkFoundByIdException;
import com.linkurlshorter.urlshortener.user.NoSuchEmailFoundException;
import com.linkurlshorter.urlshortener.user.NoUserFoundByEmailException;
import com.linkurlshorter.urlshortener.user.NoUserFoundByIdException;
Expand Down Expand Up @@ -110,5 +112,27 @@ public ResponseEntity<Object> handleNotFoundExceptions(
private ErrorResponse buildErrorResponse(HttpStatus status, String message, String requestURI) {
return new ErrorResponse(LocalDateTime.now(), status.value(), message, requestURI);
}
/**
* Handles Forbidden (403) exceptions for different types of requests.
* Returns a response with a 403 status and the corresponding error message.
*
* @param ex forbidden exception
* @param request HttpServletRequest object representing the HTTP request
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<Object> handleForbiddenException(
ForbiddenException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.FORBIDDEN,
ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
}
@ExceptionHandler(NoLinkFoundByIdException.class)
public ResponseEntity<Object> handleNoLinkFoundByIdException(
NoLinkFoundByIdException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.NOT_FOUND,
ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.linkurlshorter.urlshortener.user.User;
import com.linkurlshorter.urlshortener.user.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
Expand Down Expand Up @@ -56,6 +58,8 @@ public class LinkController {
* @see InternalServerLinkException
*/
@PostMapping("/create")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Create new link")
public ResponseEntity<CreateLinkResponse> createLink(@RequestBody @Valid CreateLinkRequest createRequest) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = userService.findByEmail(authentication.getName());
Expand Down Expand Up @@ -83,9 +87,11 @@ public ResponseEntity<CreateLinkResponse> createLink(@RequestBody @Valid CreateL
* @throws ForbiddenException if the authenticated user does not have rights to delete the link
*/
@PostMapping("/delete")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Delete link by ID")
public ResponseEntity<LinkModifyingResponse> deleteLink(@RequestParam UUID id) {
if (doesUserHaveRightsForLinkById(id)) {
linkService.deleteById(id);
linkService.deleteById(id);//TODO: add validations (id not null etc)
return ResponseEntity.ok(new LinkModifyingResponse("ok"));
} else {
throw new ForbiddenException(OPERATION_FORBIDDEN_MSG);
Expand All @@ -101,6 +107,8 @@ public ResponseEntity<LinkModifyingResponse> deleteLink(@RequestParam UUID id) {
* @throws LinkStatusException if the status of the link is not ACTIVE
*/
@PostMapping("/edit/content")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Edit link content")
public ResponseEntity<LinkModifyingResponse> editLinkContent(@RequestBody EditLinkContentRequest request) {
if (doesUserHaveRightsForLinkById(request.getId())) {
Link oldLink = linkService.findById(request.getId());
Expand All @@ -124,6 +132,8 @@ public ResponseEntity<LinkModifyingResponse> editLinkContent(@RequestBody EditLi
* @throws DeletedLinkException if the link is already deleted
*/
@PostMapping("/edit/refresh")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Refresh link expiration time")
public ResponseEntity<LinkModifyingResponse> refreshLink(@RequestParam UUID id) {
if (doesUserHaveRightsForLinkById(id)) {
Link oldLink = linkService.findById(id);
Expand All @@ -147,6 +157,8 @@ public ResponseEntity<LinkModifyingResponse> refreshLink(@RequestParam UUID id)
* @throws ForbiddenException if the authenticated user does not have rights to access the link
*/
@GetMapping("/info")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Get link info")
public ResponseEntity<LinkInfoResponse> getInfoByShortLink(@RequestParam String shortLink) {
Link link = linkService.findByShortLink(shortLink);
if (doesUserHaveRightsForLinkById(link.getId())) {
Expand All @@ -164,6 +176,8 @@ public ResponseEntity<LinkInfoResponse> getInfoByShortLink(@RequestParam String
* @return a ResponseEntity containing the response object with information about all links for the user
*/
@GetMapping("/all-links-info")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Get all links info")
public ResponseEntity<LinkInfoResponse> getAllLinksForUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UUID requesterUserId = userService.findByEmail(authentication.getName()).getId();
Expand All @@ -183,6 +197,8 @@ public ResponseEntity<LinkInfoResponse> getAllLinksForUser() {
* links are sorted in descending order
*/
@GetMapping("/url-usage-top-for-user")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Get all links usage statistics")
public ResponseEntity<LinkStatisticsResponse> getLinksStatsForUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User requesterUser = userService.findByEmail(authentication.getName());
Expand All @@ -198,15 +214,15 @@ public ResponseEntity<LinkStatisticsResponse> getLinksStatsForUser() {
*/
private String generateShortLink() {
return RandomStringUtils.randomAlphanumeric(8);
}
} //TODO: extracted into a servicethis method into service.

/**
* Checks if the authenticated user has rights to perform operations on a given link.
*
* @param linkId the UUID of the link to check
* @return true if the user has rights, false otherwise
*/
private boolean doesUserHaveRightsForLinkById(UUID linkId) { //TODO: may be transformed into @?
private boolean doesUserHaveRightsForLinkById(UUID linkId) { //TODO: may be transformed into @? and extracted into a servicethis method into service.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UUID linkUserId = linkService.findById(linkId).getUser().getId();
UUID currentUserId = userService.findByEmail(authentication.getName()).getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
Expand All @@ -32,6 +34,8 @@
@AutoConfigureMockMvc
@ExtendWith(MockitoExtension.class)
@Testcontainers
@Transactional
@Rollback
class AuthControllerIntegrationTest {
@Container
@ServiceConnection
Expand Down Expand Up @@ -134,6 +138,22 @@ void loginFailedWhenInvalidEmailGivenTest(String email) throws Exception {
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Email address entered incorrectly!"));
}

/**
* Test case to verify successful user registration.
*
* @throws Exception if any error occurs during the test
*/
@Test
void registerSuccessfulTest() throws Exception {
authRequest = new AuthRequest("[email protected]", "Pass1234");
this.mockMvc.perform(MockMvcRequestBuilders.post(baseUrl + "register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("User registered successfully!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.jwtToken").exists());
}

/**
* Parameterized test to verify registration failure with invalid passwords.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.linkurlshorter.urlshortener.link;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.linkurlshorter.urlshortener.auth.dto.AuthRequest;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.util.UUID;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
* Integration tests for the LinkController class.
*
* @author Ivan Shalaiev
* @version 1.0
*/
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(MockitoExtension.class)
@Testcontainers
@Transactional
@Rollback
class LinkControllerIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> container = new PostgreSQLContainer<>("postgres:16.0-alpine");

@Autowired
private MockMvc mockMvc;
private final String baseUrl = "/api/V1/link/";
private String token;
private AuthRequest authRequest;
private final ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() throws Exception {
authRequest = new AuthRequest("[email protected]", "Pass1234");
ResultActions result = this.mockMvc.perform(MockMvcRequestBuilders.post("/api/V1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authRequest)));
MvcResult mvcResult = result.andDo(print()).andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString();
JSONObject jsonObject = new JSONObject(contentAsString);
this.token = "Bearer " + jsonObject.getString("jwtToken");
}

@ParameterizedTest
@ValueSource(strings = {"https://www.youtube.com",
"https://open.spotify.com/",
"https://www.google.com",
"https://www.facebook.com"})
void createShortLinkWorksCorrectly(String url) throws Exception {
CreateLinkRequest createLinkRequest = new CreateLinkRequest(url);
mockMvc.perform(post(baseUrl + "create")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(objectMapper.writeValueAsString(createLinkRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.error").value("ok"))
.andExpect(jsonPath("$.shortLink").isNotEmpty());
}

@ParameterizedTest
@ValueSource(strings = {"https://www.",
"https://open.spotifycom",
"https://www.google.com@",
"https://www.facebook.com%"})
void createShortLinkFailsWhenUrlIsInvalid(String url) throws Exception {
CreateLinkRequest createLinkRequest = new CreateLinkRequest(url);
mockMvc.perform(post(baseUrl + "create")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(objectMapper.writeValueAsString(createLinkRequest)))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.statusCode").value(400))
.andExpect(jsonPath("$.message").value("Not valid format url!"))
.andExpect(jsonPath("$.path").value("/api/V1/link/create"));
}

@Test
void deleteLinkWorksCorrectly() throws Exception {
UUID id = UUID.fromString("3053e49b-6da3-4389-9d06-23b2d57b6f25");
mockMvc.perform(post(baseUrl + "delete" + "?id=" + id)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.error").value("ok"));
}
@Test
void deleteLinkFailsWhenIdIsInvalid() throws Exception {
UUID id = UUID.randomUUID();
mockMvc.perform(post(baseUrl + "delete" + "?id=" + id)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.statusCode").value(404))
.andExpect(jsonPath("$.message").value("No link by provided id found"))
.andExpect(jsonPath("$.path").value("/api/V1/link/delete"));
}
@Test
void deleteLinkFailsWhenIdIsNull() throws Exception {
mockMvc.perform(post(baseUrl + "delete" + "?id=" + null)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token))
.andExpect(status().is4xxClientError());
}
@Test
void deleteLinkFailsWhenUserHasNoRightsForThisLink() throws Exception {
authRequest = new AuthRequest("[email protected]", "Pass1234");
ResultActions result = this.mockMvc.perform(MockMvcRequestBuilders.post("/api/V1/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authRequest)));
MvcResult mvcResult = result.andDo(print()).andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString();
JSONObject jsonObject = new JSONObject(contentAsString);
this.token = "Bearer " + jsonObject.getString("jwtToken");
UUID id = UUID.fromString("3053e49b-6da3-4389-9d06-23b2d57b6f25");
mockMvc.perform(post(baseUrl + "delete" + "?id=" + id)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.statusCode").value(403))
.andExpect(jsonPath("$.message").value("Operation forbidden!"))
.andExpect(jsonPath("$.path").value("/api/V1/link/delete"));
}
}
Loading

0 comments on commit 8d392ae

Please sign in to comment.