Skip to content

Commit

Permalink
feat/36 custom error handling (#91)
Browse files Browse the repository at this point in the history
* feat: create Custom handler || handle custom exception

* feat: format error response

* feat: change error field to errorCode at ErrorResponse

* chore: change some test codes

* feat: apply pr

* feat: handle 404 , add test code

* docs: update open-api.yaml

* feat: apply pr

* feat: handle spring security exception

* style: apply lintformat

* feat: handle jwt error and add some tests

* docs: update open-api.yaml

---------

Co-authored-by: Git Actions <no-reply@github.com>
Co-authored-by: Park Jeongseop <parkjeongseop@parkjeongseop.com>
  • Loading branch information
3 people authored Sep 30, 2023
1 parent 8eee37c commit ff28981
Show file tree
Hide file tree
Showing 36 changed files with 591 additions and 247 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ subprojects {
}

tasks.jacocoTestReport {
dependsOn(tasks.test)
dependsOn(tasks.test, integrationTest)

reports {
html.required.set(true)
Expand Down
33 changes: 33 additions & 0 deletions docs/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ paths:
'*/*':
schema:
$ref: '#/components/schemas/UserDto'
/test:
post:
tags:
- test-controller
operationId: test
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SampleDTO'
required: true
responses:
"200":
description: OK
/reservations:
post:
tags:
Expand Down Expand Up @@ -161,6 +175,18 @@ paths:
type: object
additionalProperties:
type: object
/test500:
get:
tags:
- test-controller
operationId: throwError
responses:
"200":
description: OK
content:
'*/*':
schema:
type: string
/health:
get:
tags:
Expand Down Expand Up @@ -315,6 +341,13 @@ components:
createdAt:
type: string
format: date-time
SampleDTO:
required:
- text
type: object
properties:
text:
type: string
ReservationCreateRequest:
required:
- eventId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.group4.ticketingservice

import com.group4.ticketingservice.utils.exception.ErrorCodes
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

@AutoConfigureMockMvc
class GlobalExceptionHandlerTest : AbstractIntegrationTest() {
@Autowired
private lateinit var mockMvc: MockMvc

@Test
fun `GET_api_users_access_token_info should return HTTPStatus 403 Forbidden when jwt is expired`() {
// expied jwt
val jwt = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoia21qIiwiaWF0IjoxNjk1ODg1ODk5LCJleHAiOjE2OTU4ODU4OTl9.ihz4uE9xP_TUU_GOe2pG8JkpyVofST4qqbIILnBeA20"
val endpoint = "/users/access_token_info"
val mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get(endpoint)
.header("Authorization", jwt)
)

mvcResult.andExpect(MockMvcResultMatchers.status().isForbidden)
.andExpect(MockMvcResultMatchers.jsonPath("$.timestamp").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.errorCode").value(ErrorCodes.JWT_EXPIRED.errorCode))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.path").value(endpoint))
}

@Test
fun `GET_api_users_access_token_info should return HTTPStatus 401 Unauthorized when jwt signature does not match`() {
// jwt that signature does not match
val jwt = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoia21qIiwiaWF0IjoxNjk1ODg1ODk5LCJleHAiOjE2OTU4ODU4OTl9.Fer9Q0h5RpY9CHuSRWhqBfjILVEFZ0w-49j5jAg46hY"
val endpoint = "/users/access_token_info"
val mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get(endpoint)
.header("Authorization", jwt)
)

mvcResult.andExpect(MockMvcResultMatchers.status().isUnauthorized)
.andExpect(MockMvcResultMatchers.jsonPath("$.timestamp").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.errorCode").value(ErrorCodes.JWT_AUTHENTICATION_FAILED.errorCode))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.path").value(endpoint))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.group4.ticketingservice

import com.fasterxml.jackson.databind.ObjectMapper
import com.group4.ticketingservice.dto.ErrorResponseDTO
import com.group4.ticketingservice.utils.exception.ErrorCodes
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.http.HttpStatus
import org.springframework.test.context.TestPropertySource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get

@AutoConfigureMockMvc
@TestPropertySource(properties = ["spring.mvc.throw-exception-if-no-handler-found=true", "spring.web.resources.add-mappings=false"])
class ResponseFormatTest : AbstractIntegrationTest() {
@Autowired
private lateinit var mockMvc: MockMvc

@Autowired
lateinit var objectMapper: ObjectMapper

@Test
fun `check if default 404 response and ErrorResponseDTO have the same format`() {
val mvcResult = mockMvc.perform(get("/non-existing-endpoint"))
.andReturn()

val response = mvcResult.response

assertEquals(HttpStatus.NOT_FOUND.value(), response.status)

val errorResponse = objectMapper.readValue(response.contentAsString, ErrorResponseDTO::class.java)

assertNotNull(errorResponse.timestamp)
assertEquals(ErrorCodes.END_POINT_NOT_FOUND.errorCode, errorResponse.errorCode)
assertNotNull(errorResponse.message)
assertEquals("/non-existing-endpoint", errorResponse.path)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,4 @@ class UserControllerTest : AbstractIntegrationTest() {
mockMvc.perform(MockMvcRequestBuilders.get("/users/access_token_info"))
resultActions.andExpect(status().isUnauthorized)
}

@Test
fun `GET_api_users_access_token_info should return HTTPStatus 401 Unauthorized when jwt is expired`() {
val jwt = tokenProvider.createWrongTokenForTest("${testFields.testUserName}:USER")
val resultActions: ResultActions =
mockMvc.perform(
MockMvcRequestBuilders.get("/users/access_token_info")
.header("Authorization", jwt)
)
resultActions.andExpect(status().isUnauthorized)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ class UserRepositoryTest(
@Test
fun `userRepository_existByEmail return false when user not exist`() {
// given

// when
val isUserExist = userRepository.existsByEmail(sampleUser.email)
// then
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package com.group4.ticketingservice.config
import com.google.gson.FieldNamingPolicy
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.group4.ticketingservice.utils.OffsetDateTimeAdapter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.OffsetDateTime

@Configuration
class GsonConfig {
@Bean
fun gson(): Gson {
return GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(OffsetDateTime::class.java, OffsetDateTimeAdapter())
.create()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.group4.ticketingservice.config

import com.group4.ticketingservice.JwtAuthorizationEntryPoint
import com.google.gson.Gson
import com.group4.ticketingservice.filter.JwtAuthenticationFilter
import com.group4.ticketingservice.filter.JwtAuthorizationEntryPoint
import com.group4.ticketingservice.filter.JwtAuthorizationFilter
import com.group4.ticketingservice.utils.TokenProvider
import org.springframework.context.annotation.Bean
Expand All @@ -18,11 +19,10 @@ import org.springframework.security.web.DefaultSecurityFilterChain
@Configuration
class SecurityConfig(
private val jwtAuthorizationEntryPoint: JwtAuthorizationEntryPoint,
private val tokenProvider: TokenProvider
private val tokenProvider: TokenProvider,
private val gson: Gson
) {

private val allowedUrls = arrayOf("/", "/api-docs.yaml", "/health", "/users/signup", "/bookmarks/**", "/reservations/**", "/events/**", "/actuator/**")

@Bean
fun filterChain(http: HttpSecurity): DefaultSecurityFilterChain {
http
Expand All @@ -31,24 +31,27 @@ class SecurityConfig(
.httpBasic { it.disable() }
.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers(*allowedUrls).permitAll()
.anyRequest().authenticated()
it.requestMatchers("/reservations/**").authenticated()
it.requestMatchers("/bookmarks/**").authenticated()
it.requestMatchers("/users/access_token_info").authenticated()
it.anyRequest().permitAll()
}
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.exceptionHandling { it.authenticationEntryPoint(jwtAuthorizationEntryPoint) }
.apply(CustomFilterConfigurer(tokenProvider, jwtAuthorizationEntryPoint))
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.apply(CustomFilterConfigurer(tokenProvider, jwtAuthorizationEntryPoint, gson))

return http.build()!!
}

class CustomFilterConfigurer(
private val tokenProvider: TokenProvider,
private val jwtAuthorizationEntryPoint: JwtAuthorizationEntryPoint
private val jwtAuthorizationEntryPoint: JwtAuthorizationEntryPoint,
private val gson: Gson
) :
AbstractHttpConfigurer<CustomFilterConfigurer?, HttpSecurity?>() {
override fun configure(builder: HttpSecurity?) {
val authenticationManager = builder?.getSharedObject(AuthenticationManager::class.java)
val jwtAuthenticationFilter = JwtAuthenticationFilter(authenticationManager, tokenProvider)
val jwtAuthenticationFilter = JwtAuthenticationFilter(authenticationManager, tokenProvider, gson)
jwtAuthenticationFilter.setFilterProcessesUrl("/users/signin")
val jwtAuthorizationFilter = JwtAuthorizationFilter(authenticationManager, jwtAuthorizationEntryPoint, tokenProvider)
builder?.addFilter(jwtAuthorizationFilter)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.group4.ticketingservice.controller

import com.group4.ticketingservice.utils.exception.CustomException
import com.group4.ticketingservice.utils.exception.ErrorCodes
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class TestController {
@GetMapping("/test500")
fun throwError(): String {
throw RuntimeException("This is a test error.")
}

@PostMapping("/test")
fun test(@RequestBody request: SampleDTO) {
throw CustomException(ErrorCodes.TEST_ERROR)
}
}

data class SampleDTO(
val text: String
)
18 changes: 18 additions & 0 deletions src/main/kotlin/com/group4/ticketingservice/dto/ResponseDTO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.group4.ticketingservice.dto

import java.time.OffsetDateTime

data class SuccessResponseDTO(
val timestamp: OffsetDateTime = OffsetDateTime.now(),
val message: String,
val data: Any,
val path: String

)

data class ErrorResponseDTO(
val timestamp: OffsetDateTime = OffsetDateTime.now(),
val errorCode: Int,
val message: String,
val path: String
)
Loading

0 comments on commit ff28981

Please sign in to comment.