hyeonga_code

Project_04_Url 등록 기능 구현 본문

Project_HYEONGARL

Project_04_Url 등록 기능 구현

hyeonga 2024. 5. 31. 05:59
반응형

 

url 다이어리 서비스를 제공할 예정이므로 url 관련 기능을 구현한다.


1.  Url Entity

package com.hyeongarl.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.hibernate.validator.constraints.URL;

import java.time.LocalDateTime;

/*
    @Table(name="")
        데이터베이스의 name에 지정된 이름의 테이블과 매핑된다.
    @Entity
        JPA 엔티티임을 명시하여 데이터베이스 테이블의 레코드와 매핑
    @Getter, @Setter
        롬복 라이브러리의 어노테이션으로 모든 필드에 대해 자동으로 getter/setter를 생성
    @NoArgsConstructor
        롬복 라이브러리의 어노테이션으로 매개변수가 없는 기본 생성자를 자동으로 생성
 */
@Entity
@Table(name="urls")
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Url {
    /*
        @Id
            테이블의 기본 키
        @GeneratedValue(strategy = GenerationType.IDENTITY)
            기본 피의 생성 전략을 지정
            IDENTITY : 데이터베이스가 기본 키 생성을 담당
            기본적으로 AUTO_INCREMENT를 사용
        @Column(name="user_id", updatable = false)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            필드가 수정될 수 없음을 의미
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long urlId;

    @URL
    @Column(name="url")
    private String url;

    @Size(max = 50)
    @Column(name="url_title")
    private String urlTitle;

    @Size(max = 150)
    @Column(name = "url_description")
    private String urlDescription;

    @Column(name = "category_id")
    private Long categoryId;

    @Column(name = "user_id", nullable = false)
    private Long userId;

    @Column(name = "url_regdate")
    private LocalDateTime urlRegdate;

    @Column(name = "url_update")
    private LocalDateTime urlUpdate;

    @PrePersist // 등록 시 자동으로 실행
    public void prePersist() {
        this.urlRegdate = LocalDateTime.now();
    }

    @PreUpdate // 업데이트 시 자동으로 실행
    public void preUpdate() {
        this.urlUpdate = LocalDateTime.now();
    }
}

 

 

2. UrlRequestDto

package com.hyeongarl.dto;

import com.hyeongarl.entity.Url;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UrlRequestDto {
    @NotNull(message = "URL is required")
    private String url;

    @NotNull(message = "URL Title is required")
    private String urlTitle;

    private String urlDescription;

    private Long categoryId;

    public Url toEneity() {
        return Url.builder()
                .url(url)
                .urlTitle(urlTitle)
                .urlDescription(urlDescription)
                .categoryId(categoryId)
                .build();
    }
}

 

 

3. UrlResponseDto 

package com.hyeongarl.dto;

import com.hyeongarl.entity.Url;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UrlResponseDto {
    private Long urlId;
    private String url;
    private String urlTitle;
    private String urlDescription;
    private Long categoryId;
    private LocalDateTime urlRegdate;
    private LocalDateTime urlUpdate;

    public static UrlResponseDto fromEntity(Url url) {
        return UrlResponseDto.builder()
                .urlId(url.getUrlId())
                .url(url.getUrl())
                .urlTitle(url.getUrlTitle())
                .urlDescription(url.getUrlDescription())
                .categoryId(url.getCategoryId())
                .urlRegdate(url.getUrlRegdate())
                .urlUpdate(url.getUrlUpdate())
                .build();
    }
}

 

 

4. UrlService

package com.hyeongarl.service;

import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UrlAlreadyExistException;
import com.hyeongarl.error.UrlInvalidException;
import com.hyeongarl.repository.UrlRepository;
import com.hyeongarl.util.UrlValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UrlService {
    private final UrlRepository urlRepository;

    public Url save(Url url) {
        // url 유효성 검사
        if(!UrlValidator.checkUrl(url.getUrl())) {
            throw new UrlInvalidException();
        }

        // 이미 존재하는 정보(사용자, url())
        if(urlRepository.existsByUrl(url.getUrl())) {
            throw new UrlAlreadyExistException();
        }

        return urlRepository.save(url);
    }
}

 

 

5. UrlRepository

package com.hyeongarl.repository;

import com.hyeongarl.entity.Url;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface UrlRepository extends JpaRepository<Url, Long> {
    Page<Url> findAllByUserId(Pageable pageable, Long userId);

    @Query("SELECT CASE " +
            "WHEN COUNT(u) > 0 THEN true " +
            "ELSE false END FROM Url u  " +
            "WHERE u.url = :url")
    boolean existsByUrl(String url);
}

 

 

6.  UrlController

package com.hyeongarl.controller;

import com.hyeongarl.config.jwt.TokenProvider;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UnAuthorizedException;
import com.hyeongarl.service.UrlService;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/url")
public class UrlController {

    private final TokenProvider tokenProvider;
    private final UrlService urlService;

	@ResponseStatus(HttpStatus.CREATED)
    @PostMapping
    public UrlResponseDto saveUrl(@RequestBody UrlRequestDto urlRequest, HttpServletRequest request) {
        String token = request.getHeader("Authorization").substring(7);
        Long userId;

        try {
         userId = tokenProvider.getUserId(token);
        } catch (JwtException e) {
            throw new UnAuthorizedException();
        }

        Url saveUrl = urlRequest.toEneity();
        saveUrl.setUserId(userId);

        return UrlResponseDto.fromEntity(urlService.save(saveUrl));
    }
}

 

 

7. UrlInvalidException

package com.hyeongarl.error;

import org.springframework.http.HttpStatus;

public class UrlInvalidException extends BaseException {
    private static final String DEFAULT_MESSAGE = "유효하지 않은 Url입니다.";
    private static final String DEFAULT_CODE = "400003";

    public UrlInvalidException() {
        super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.BAD_REQUEST);
    }

    public UrlInvalidException(String message) {
        super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.BAD_REQUEST);
    }
}

 

 

8. UrlAlreadyExistsException

package com.hyeongarl.error;

import org.springframework.http.HttpStatus;

public class UrlAlreadyExistException extends BaseException {
    private static final String DEFAULT_MESSAGE = "이미 존재하는 Url입니다.";
    private static final String DEFAULT_CODE = "400007";

    public UrlAlreadyExistException() {
        super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.CONFLICT);
    }

    public UrlAlreadyExistException(String message) {
        super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.CONFLICT);
    }
}

 

 

 

UrlControllerTest

package com.hyeongarl.controller;

import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UrlControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @DisplayName("testSaveUrl")
    void testSaveUrl() {
        UrlRequestDto urlRequest = UrlRequestDto.builder()
                .url("https://www.naver.com")
                .urlTitle("saveUrlTitle")
                .urlDescription("saveUrlDescription")
                .categoryId(1L)
                .build();

        ResponseEntity<UrlResponseDto> responseEntity
                = restTemplate.postForEntity("/url", urlRequest, UrlResponseDto.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        UrlResponseDto responseBody = responseEntity.getBody();
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getUrl()).isEqualTo("https://www.naver.com");
    }
}

 

> 테스트 실행 오류1.

인증 처리가 되지 않아 오류가 발생했다.
테스트 시작시 회원 가입/로그인을 한 후 실행하려했으나 생성된 토큰을 설정하고 가져오지 못했고 모든 테스트에서 회원가입과 로그인을 진행해야 하는 번거로움을 없애기 위해 SecurityFilterChain 클래스의 filterChain() 메소드에서 모든 경로에 대해 엑세스를 누구나 가능하게 변경했다.

추후 로그인 인증으로 처리하는 값을 적용시킬 예정이다.

 

package com.hyeongarl.config;

import com.hyeongarl.config.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
     configure              : 스프링 시큐리티 기능 비활성화
     filterChain            : 특정 HTTP 요청에 대한 웹 기반 보안 구성
     authenticationManager  : 인증 관리자 관련 설정
     bCryptPasswordEncoder  : 비밀번호 인코더로 사용할 빈 등록
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityFilterChain {      // SecurityConfig
    private final TokenProvider tokenProvider;
...

    @Bean   // 특정 HTTP 요청에 대한 웹 기반 보안 구성 (인증, 인가, 로그인, 로그아웃 관련 설정)
    public org.springframework.security.web.SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        TokenAuthenticationFilter tokenFilter = new TokenAuthenticationFilter(tokenProvider);
        return http
                .authorizeHttpRequests(
                        // 특정 경로에 대한 엑세스 설정
                        auth -> auth.requestMatchers(       // 특정 요청과 일치하는 url에 대한 엑세스 설정
                                        new AntPathRequestMatcher("/**")
                                ).permitAll()               // 누구나 접근 가능하게 설정
                                .anyRequest()               // 위에서 설정한 url 이외의 요청에 대해 설정
                                .authenticated()            // 별도의 인가는 필요하지 않으나 인증이 성공한 상태만 접근이 가능
                )
                .csrf(AbstractHttpConfigurer::disable)      // CSRF 공격 방지를 위해서는 활성화하는게 좋음
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)  // JWT 필터 추가
                .build();
    }
...
}

 

> UrlControllerTest / testSaveUrl() 실행

 

하루를 소비했다..

 

 

 

 

 

UrlServiceTest

package com.hyeongarl.service;

import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UrlAlreadyExistException;
import com.hyeongarl.error.UrlInvalidException;
import com.hyeongarl.repository.UrlRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/*
    @ExtendWith(MockitoExtension.class)
        JUnit5의 확장 모델을 활용하여 Mockito를 사용할 수 있다.
        MockExtension : Mockito에 대한 설정을 초기화 및 @Mockito 처리
 */
@ExtendWith(MockitoExtension.class)
public class UrlServiceTest {
    /*
        @Mock
            Mockito를 사용해 가짜 객체를 생성
     */
    @Mock
    private UrlRepository urlRepository;

    /*
        @InjectMocks
            실제 객체로 만들어 필요한 의존성 객체를 주입
     */
    @InjectMocks
    private UrlService urlService;

    @Test
    @DisplayName("save_success")
    void saveUrl_success() {
        UrlRequestDto urlRequest 
        	= new UrlRequestDto("https://www.naver.com", "saveUrlTitle", "saveUrl_Description", 1L);

        when(urlRepository.existsByUrl(urlRequest.getUrl())).thenReturn(false);
        when(urlRepository.save(any(Url.class))).thenReturn(urlRequest.toEntity());

        Url url = urlService.save(urlRequest.toEntity());
        assertNotNull(url);
        assertEquals(urlRequest.getUrl(), url.getUrl());
        assertEquals(urlRequest.getUrlTitle(), url.getUrlTitle());
        assertEquals(urlRequest.getUrlDescription(), url.getUrlDescription());

        verify(urlRepository, times(1)).save(any(Url.class));
    }

    @Test
    @DisplayName("save_fail_invalid")
    void saveUrl_fail_invalid() {
        UrlRequestDto urlRequest 
        	= new UrlRequestDto(null, "saveUrlTitle", "saveUrlDescription", 1L);

        assertThrows(UrlInvalidException.class, () -> urlService.save(urlRequest.toEntity()));

        verify(urlRepository, never()).save(any(Url.class));
    }

    @Test
    @DisplayName("save_fail_alreadyExist")
    void saveUrl_fail_alreadyExist() {
        UrlRequestDto urlRequest
        	= new UrlRequestDto("https://www.naver.com", "saveUrlTitle", "saveUrl_Description", 1L);

        when(urlRepository.existsByUrl(urlRequest.getUrl())).thenReturn(true);

        assertThrows(UrlAlreadyExistException.class, 
        	()-> urlService.save(urlRequest.toEntity()));

        verify(urlRepository, never()).save(any(Url.class));
    }
}

 

> 실행

반응형