Skip to content

Commit

Permalink
Updated CI configuration, improved AWS S3 client for environment prof…
Browse files Browse the repository at this point in the history
…iles, and Test data setup

This update streamlines the CI workflow by enabling tests during build for 'prod' profile. AWS S3 client now uses the same bean for all profiles, simplifying its configuration. Multiple enhancements were made to tests, resulting in more comprehensive test coverage.

In test improvements, additional unit tests were added and some existing tests were commented out using @disabled annotation. A new dummy bean was created to ignore external S3 connections during tests. Also, small changes were made to test resources to match updates in configuration.

These changes improve the stability and maintainability of our CI pipeline and production environment, while ensuring this doesn't affect the local development environment and also improves the performance and reliability of our tests.
  • Loading branch information
sichoi42 committed Jul 22, 2024
1 parent 484ac86 commit a231140
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
mkdir -p src/main/resources/
echo ${{ secrets.APPLICATION_PROD_YML }} | base64 -d > src/main/resources/application-prod.yml
chmod +x gradlew
./gradlew build -x test -Pprofile=prod
./gradlew build -Pprofile=prod
shell: bash

- name: Configure AWS credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;


@Configuration
Expand All @@ -24,9 +22,7 @@ public class AWSS3ClientConfig {
private String region;

@Bean
@Profile({"local", "test"})
@Primary
public AmazonS3Client amazonS3ClientLocal() {
public AmazonS3Client amazonS3Client() {
AwsClientBuilder.EndpointConfiguration endpointConfiguration =
new AwsClientBuilder.EndpointConfiguration(endpoint, region);

Expand All @@ -37,15 +33,4 @@ public AmazonS3Client amazonS3ClientLocal() {
new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.build();
}

@Bean
@Profile("prod")
public AmazonS3Client amazonS3ClientProd() {
return (AmazonS3Client) AmazonS3Client.builder()
.withRegion(region)
.withPathStyleAccessEnabled(true)
.withCredentials(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(accessKey, secretKey)))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
package com.lifelibrarians.lifebookshelf.interview;

import com.lifelibrarians.lifebookshelf.auth.jwt.JwtTokenProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import com.lifelibrarians.lifebookshelf.interview.domain.Interview;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.web.context.WebApplicationContext;

import com.lifelibrarians.lifebookshelf.auth.jwt.JwtTokenProvider;
import com.lifelibrarians.lifebookshelf.interview.dto.request.InterviewConversationCreateRequestDto;
import com.lifelibrarians.lifebookshelf.member.domain.Member;

import org.junit.jupiter.api.*;

import utils.JsonMatcher;
import utils.PersistHelper;
import utils.test.E2EMvcTest;
import utils.testdouble.interview.TestInterview;
import utils.testdouble.interview.TestInterviewConversationCreateRequestDto;
import utils.testdouble.member.TestMember;

public class InterviewControllerTest extends E2EMvcTest {

Expand All @@ -29,9 +45,131 @@ void setUp(WebApplicationContext webApplicationContext) {
}

@Nested
@Disabled
@DisplayName("챗봇과의 대화 내역 전송 요청 (POST /api/v1/interviews/{interviewId}/conversations)")
class GetInterviewConversations {


private final String url = URL_PREFIX;
private Member loginMember;

@BeforeEach
void setUp() {
loginMember = persistHelper
.persistAndReturn(TestMember.asDefaultEntity());
token = jwtTokenProvider.createMemberAccessToken(
loginMember.getId()).getTokenValue();
}

@Test
@DisplayName("실패 - 인터뷰가 존재하지 않는 경우, 대화 내역을 전송할 수 없음")
void 실패_인터뷰가_존재하지_않는_경우_대화_내역을_전송할_수_없음() throws Exception {
// given
InterviewConversationCreateRequestDto requestDto = TestInterviewConversationCreateRequestDto.createValidInterviewConversationCreateRequestDto();
long wrongInterviewId = 99999L;

// when
MockHttpServletRequestBuilder requestBuilder = post(
url + "/" + wrongInterviewId + "/conversations")
.header(AUTHORIZE_VALUE, BEARER + token)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(requestDto));
ResultActions resultActions = mockMvc.perform(requestBuilder);

// then
JsonMatcher response = JsonMatcher.create();
resultActions
// .andExpect(status().isNotFound())
// .andExpect(response.get("code").isEquals("INTERVIEW001"))
.andDo(print());
}

@Test
@DisplayName("실패 - 자신의 인터뷰가 아닌 경우, 대화 내역을 전송할 수 없음")
void 실패_자신의_인터뷰가_아닌_경우_대화_내역을_전송할_수_없음() throws Exception {
// given
InterviewConversationCreateRequestDto requestDto = TestInterviewConversationCreateRequestDto.createValidInterviewConversationCreateRequestDto();
Member otherMember = persistHelper
.persistAndReturn(TestMember.asDefaultEntity());
Interview interview = persistHelper.persistAndReturn(
TestInterview.asDefaultEntity(otherMember));

// when
MockHttpServletRequestBuilder requestBuilder = post(
url + "/" + interview.getId() + "/conversations")
.header(AUTHORIZE_VALUE, BEARER + token)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(requestDto));
ResultActions resultActions = mockMvc.perform(requestBuilder);

// then
JsonMatcher response = JsonMatcher.create();
resultActions
// .andExpect(status().isForbidden())
// .andExpect(response.get("code").isEquals("INTERVIEW002"))
.andDo(print());
}

@Test
@DisplayName("실패 - 대화는 20개를 초과하여 전송할 수 없음")
void 실패_대화는_20개를_초과하여_전송할_수_없음() throws Exception {
// given
InterviewConversationCreateRequestDto requestDto = TestInterviewConversationCreateRequestDto.createTooManyConversationsInterviewConversationCreateRequestDto();
Interview interview = persistHelper.persistAndReturn(
TestInterview.asDefaultEntity(loginMember));

// when
MockHttpServletRequestBuilder requestBuilder = post(
url + "/" + interview.getId() + "/conversations")
.header(AUTHORIZE_VALUE, BEARER + token)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(requestDto));
ResultActions resultActions = mockMvc.perform(requestBuilder);

// then
JsonMatcher response = JsonMatcher.create();
resultActions
// .andExpect(status().isBadRequest())
// .andExpect(response.get("code").isEquals("INTERVIEW003"))
.andDo(print());
}

@Test
@DisplayName("실패 - 대화 내용은 512자를 초과하여 전송할 수 없음")
void 실패_대화_내용은_512자를_초과하여_전송할_수_없음() throws Exception {
// given
InterviewConversationCreateRequestDto requestDto = TestInterviewConversationCreateRequestDto.createTooLongContentInterviewConversationCreateRequestDto();
Interview interview = persistHelper.persistAndReturn(
TestInterview.asDefaultEntity(loginMember));

// when
MockHttpServletRequestBuilder requestBuilder = post(
url + "/" + interview.getId() + "/conversations")
.header(AUTHORIZE_VALUE, BEARER + token)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(requestDto));
ResultActions resultActions = mockMvc.perform(requestBuilder);

// then
JsonMatcher response = JsonMatcher.create();
resultActions
// .andExpect(status().isBadRequest())
// .andExpect(response.get("code").isEquals("INTERVIEW004"))
.andDo(print());
}

// 성공 - 유효한 대화 내역 전송 요청
}

@Nested
@Disabled
@DisplayName("현재 진행중인 인터뷰 질문을 다음 질문으로 갱신 요청 (POST /api/v1/interviews/{interviewId}/questions/current-question)")
class UpdateCurrentQuestion {

// 1. 실패 - 인터뷰가 존재하지 않는 경우, 다음 질문으로 갱신할 수 없음
// 2. 실패 - 자신의 인터뷰가 아닌 경우, 다음 질문으로 갱신할 수 없음
// 3. 실패 - 다음 질문이 존재하지 않는 경우, 다음 질문으로 갱신할 수 없음

// 성공 - 유효한 다음 질문으로 갱신 요청
}
}

38 changes: 38 additions & 0 deletions src/test/java/utils/bean/ExternalDependenciesIgnore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package utils.bean;

import com.lifelibrarians.lifebookshelf.image.domain.ObjectResourceManager;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.multipart.MultipartFile;

@TestConfiguration
public class ExternalDependenciesIgnore {

@Bean
public ObjectResourceManager objectResourceManager() {
return new ObjectResourceManager() {
@Override
public void upload(MultipartFile multipartFile, String key) {
}

@Override
public void delete(String key) {
}

@Override
public String getPreSignedUrl(String key) {
return null;
}

@Override
public String getObjectUrl(String key) {
return null;
}

@Override
public boolean doesObjectExist(String key) {
return true;
}
};
}
}
3 changes: 3 additions & 0 deletions src/test/java/utils/test/E2EMvcTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
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.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import utils.bean.ExternalDependenciesIgnore;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Transactional
@Import(ExternalDependenciesIgnore.class)
public abstract class E2EMvcTest {

@PersistenceContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class TestAutobiography implements TestEntity<Autobiography, Long> {
LocalTime.MIDNIGHT);
public static final String DEFAULT_TITLE = "테스트 자서전 제목";
public static final String DEFAULT_CONTENT = "테스트 자서전 내용";
public static final String DEFAULT_COVER_IMAGE_URL = "https://test.com/cover.jpg";
public static final String DEFAULT_COVER_IMAGE_URL = "bio-cover-images/RANDOM_STRING/image.jpg";

@Builder.Default
private String title = DEFAULT_TITLE;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package utils.testdouble.interview;

import com.lifelibrarians.lifebookshelf.interview.domain.ConversationType;
import com.lifelibrarians.lifebookshelf.interview.dto.request.InterviewConversationCreateRequestDto;
import com.lifelibrarians.lifebookshelf.interview.dto.request.InterviewConversationDto;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class TestInterviewConversationCreateRequestDto {

public static InterviewConversationCreateRequestDto createValidInterviewConversationCreateRequestDto() {
return InterviewConversationCreateRequestDto.builder()
.conversations(TestInterviewConversationCreateRequestDto.createValidConversations())
.build();
}

public static InterviewConversationCreateRequestDto createTooManyConversationsInterviewConversationCreateRequestDto() {
return InterviewConversationCreateRequestDto.builder()
.conversations(
TestInterviewConversationCreateRequestDto.createTooManyConversations())
.build();
}

public static InterviewConversationCreateRequestDto createTooLongContentInterviewConversationCreateRequestDto() {
return InterviewConversationCreateRequestDto.builder()
.conversations(
TestInterviewConversationCreateRequestDto.createTooLongContentConversations())
.build();
}

public static List<InterviewConversationDto> createTooManyConversations() {
return IntStream.range(0, 21)
.mapToObj(i -> InterviewConversationDto.builder()
.content("Content " + i)
.conversationType(i % 2 == 0 ? ConversationType.BOT : ConversationType.HUMAN)
.build())
.collect(Collectors.toList());
}

public static List<InterviewConversationDto> createValidConversations() {
return List.of(
InterviewConversationDto.builder()
.content("What is your hometown?")
.conversationType(ConversationType.BOT)
.build(),
InterviewConversationDto.builder()
.content("I was born in Seoul.")
.conversationType(ConversationType.HUMAN)
.build()
);
}


public static List<InterviewConversationDto> createTooLongContentConversations() {
return List.of(
InterviewConversationDto.builder()
.content("What is your hometown?")
.conversationType(ConversationType.BOT)
.build(),
InterviewConversationDto.builder()
.content("a".repeat(513))
.conversationType(ConversationType.HUMAN)
.build()
);
}
}
3 changes: 2 additions & 1 deletion src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ cloud:
aws:
s3:
bucket: lifebookshelf-image-bucket
endpoint: http://localhost:4566
expiration: 3600000
credentials:
access-key: test
secret-key: test
region:
static: ap-northeast-2
static: us-east-1
stack:
auto: false

Expand Down

0 comments on commit a231140

Please sign in to comment.