test 추가
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -82,6 +82,11 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ mkdir -p "$LOG_DIR"
|
||||
# 시작
|
||||
echo "[$APP_NAME] 시작 중..."
|
||||
nohup "$JAVA_HOME/bin/java" \
|
||||
-DAPP_HASH_SALT="${APP_HASH_SALT}" \
|
||||
-jar "$JAR" \
|
||||
--spring.profiles.active=prod \
|
||||
> /dev/null 2>&1 &
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.owrawww.domain.mapper")
|
||||
@MapperScan({"com.owrawww.domain.mapper", "com.owrawww.migration"})
|
||||
public class OwrawwwApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -45,5 +45,7 @@ public class Inquiry {
|
||||
private Integer subGubun;
|
||||
private Integer depth;
|
||||
private String solutionGubun;
|
||||
private String telHash; // SHA-256(솔트+tel) - 검색/중복체크용
|
||||
private String emailHash; // SHA-256(솔트+email) - 검색/중복체크용
|
||||
|
||||
}
|
||||
|
||||
21
src/main/java/com/owrawww/migration/DataMigrationMapper.java
Normal file
21
src/main/java/com/owrawww/migration/DataMigrationMapper.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.owrawww.migration;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface DataMigrationMapper {
|
||||
|
||||
/** 미암호화 레코드 조회 (phone 길이 30자 이하 = 평문) */
|
||||
List<Map<String, Object>> selectPlainRecords();
|
||||
|
||||
/** phone, tel_hash, email, email_hash 일괄 업데이트 */
|
||||
int updateEncrypted(@Param("id") Long id,
|
||||
@Param("phone") String phone,
|
||||
@Param("telHash") String telHash,
|
||||
@Param("email") String email,
|
||||
@Param("emailHash") String emailHash);
|
||||
}
|
||||
81
src/main/java/com/owrawww/migration/DataMigrationRunner.java
Normal file
81
src/main/java/com/owrawww/migration/DataMigrationRunner.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.owrawww.migration;
|
||||
|
||||
import com.owrawww.util.AesUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 기존 평문 phone/email 데이터를 일괄 암호화하는 마이그레이션 Runner.
|
||||
*
|
||||
* 실행 방법 (운영 서버):
|
||||
* java -jar owrawww.jar --spring.profiles.active=prod,migration
|
||||
*
|
||||
* 주의사항:
|
||||
* - 한 번만 실행해야 합니다. 재실행 시 이미 암호화된 데이터는 자동으로 건너뜁니다.
|
||||
* - 실행 전 DB 백업을 반드시 수행하세요.
|
||||
* - 마이그레이션 완료 후 이 profile 없이 앱을 재시작하세요.
|
||||
*/
|
||||
@Slf4j
|
||||
@Profile("migration")
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DataMigrationRunner implements ApplicationRunner {
|
||||
|
||||
private final DataMigrationMapper migrationMapper;
|
||||
private final AesUtil aesUtil;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
log.info("=== [마이그레이션 시작] phone/email 암호화 ===");
|
||||
|
||||
List<Map<String, Object>> records = migrationMapper.selectPlainRecords();
|
||||
log.info("암호화 대상 레코드 수: {}건", records.size());
|
||||
|
||||
if (records.isEmpty()) {
|
||||
log.info("암호화할 평문 데이터가 없습니다. 이미 완료됐거나 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
int success = 0;
|
||||
int skip = 0;
|
||||
|
||||
for (Map<String, Object> row : records) {
|
||||
Long id = ((Number) row.get("id")).longValue();
|
||||
String phone = (String) row.get("phone");
|
||||
String email = (String) row.get("email");
|
||||
|
||||
// null/빈값 방어
|
||||
if (phone == null || phone.isBlank() || email == null || email.isBlank()) {
|
||||
log.warn(" ID={} : phone 또는 email 이 비어있어 건너뜁니다.", id);
|
||||
skip++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
String encPhone = aesUtil.encrypt(phone);
|
||||
String hashPhone = aesUtil.hash(phone);
|
||||
String encEmail = aesUtil.encrypt(email);
|
||||
String hashEmail = aesUtil.hash(email);
|
||||
|
||||
migrationMapper.updateEncrypted(id, encPhone, hashPhone, encEmail, hashEmail);
|
||||
log.info(" ID={} : 암호화 완료", id);
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
log.error(" ID={} : 암호화 실패 - {}", id, e.getMessage());
|
||||
throw new RuntimeException("ID=" + id + " 처리 중 오류 발생. 트랜잭션 롤백.", e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("=== [마이그레이션 완료] 성공: {}건, 건너뜀: {}건 ===", success, skip);
|
||||
log.info("마이그레이션이 끝났습니다. 'migration' 프로파일 없이 앱을 재시작하세요.");
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ public class InquiryService {
|
||||
savedInquiry.setComment(inquiry.getContent());
|
||||
savedInquiry.setName(inquiry.getName());
|
||||
savedInquiry.setTel(aesUtil.encrypt(inquiry.getTel()));
|
||||
savedInquiry.setTelHash(aesUtil.hash(inquiry.getTel()));
|
||||
savedInquiry.setEmail(aesUtil.encrypt(inquiry.getEmail()));
|
||||
savedInquiry.setEmailHash(aesUtil.hash(inquiry.getEmail()));
|
||||
savedInquiry.setTopCode(2);
|
||||
savedInquiry.setLeftCode(1);
|
||||
savedInquiry.setSubGubun(1);
|
||||
|
||||
@@ -7,8 +7,12 @@ import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* AES-256-GCM 암호화/복호화 유틸리티
|
||||
@@ -22,13 +26,17 @@ public class AesUtil {
|
||||
private static final int TAG_BIT_LEN = 128; // GCM 인증 태그 크기
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final String hashSalt;
|
||||
|
||||
public AesUtil(@Value("${app.encryption.key}") String base64Key) {
|
||||
public AesUtil(
|
||||
@Value("${app.encryption.key}") String base64Key,
|
||||
@Value("${app.hash.salt}") String hashSalt) {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
|
||||
if (keyBytes.length != 32) {
|
||||
throw new IllegalArgumentException("암호화 키는 Base64 인코딩된 32바이트(256bit)여야 합니다.");
|
||||
}
|
||||
this.secretKey = new SecretKeySpec(keyBytes, "AES");
|
||||
this.hashSalt = hashSalt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,4 +83,22 @@ public class AesUtil {
|
||||
throw new RuntimeException("복호화 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 평문 → SHA-256(솔트 + 평문) → HEX 문자열 (검색/중복체크용)
|
||||
* - 단방향: 복호화 불가
|
||||
* - 솔트로 레인보우 테이블 공격 방어
|
||||
*/
|
||||
public String hash(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) return plainText;
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashBytes = digest.digest(
|
||||
(hashSalt + plainText).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
return HexFormat.of().formatHex(hashBytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 알고리즘을 사용할 수 없습니다.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ app:
|
||||
path: /home/www/owrainfo/uploads
|
||||
encryption:
|
||||
key: PIYAsB81yr7uETVoM/E7Sz9Sp5kNB46uHyjnfQE3XOs= # 운영 전용 키로 교체 필요 (base64 인코딩된 32바이트)
|
||||
|
||||
hash:
|
||||
salt: ${APP_HASH_SALT:9C4622A0E11849F94C490FCE1E6114F05BFF98FCEB5B85067F09E35ADD0D3FAB} # 반드시 환경변수로 주입 (서버에서 설정)
|
||||
@@ -24,4 +24,7 @@ logging:
|
||||
app:
|
||||
upload:
|
||||
path: /home/www/owrainfo/uploads
|
||||
|
||||
encryption:
|
||||
key: PIYAsB81yr7uETVoM/E7Sz9Sp5kNB46uHyjnfQE3XOs= # 운영 전용 키로 교체 필요 (base64 인코딩된 32바이트)
|
||||
hash:
|
||||
salt: ${APP_HASH_SALT:9C4622A0E11849F94C490FCE1E6114F05BFF98FCEB5B85067F09E35ADD0D3FAB}
|
||||
|
||||
@@ -32,3 +32,5 @@ app:
|
||||
path: D:/uploads/owrawww/careers # 기본값 (프로파일별로 override)
|
||||
encryption:
|
||||
key: PIYAsB81yr7uETVoM/E7Sz9Sp5kNB46uHyjnfQE3XOs= # AES-256 키 (운영에서는 반드시 별도 키로 교체)
|
||||
hash:
|
||||
salt: ${APP_HASH_SALT:3DBD3CD58A81C407AD0D3E10515DAD7544E70911A2E43EF3A07042B102D41716} # 환경변수 우선, 없으면 기본값
|
||||
|
||||
29
src/main/resources/mapper/DataMigrationMapper.xml
Normal file
29
src/main/resources/mapper/DataMigrationMapper.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.owrawww.migration.DataMigrationMapper">
|
||||
|
||||
<!--
|
||||
평문 레코드 판별 기준:
|
||||
- 암호화된 값: AES-GCM(12바이트 IV + 데이터 + 16바이트 태그) → Base64 → 최소 약 50자 이상
|
||||
- 평문 전화번호: 최대 20자 (예: 010-1234-5678)
|
||||
- LENGTH(phone) <= 30 이면 아직 평문으로 판단
|
||||
-->
|
||||
<select id="selectPlainRecords" resultType="map">
|
||||
SELECT id, phone, email
|
||||
FROM application_table
|
||||
WHERE LENGTH(phone) <= 30
|
||||
ORDER BY id
|
||||
</select>
|
||||
|
||||
<update id="updateEncrypted">
|
||||
UPDATE application_table
|
||||
SET phone = #{phone},
|
||||
tel_hash = #{telHash},
|
||||
email = #{email},
|
||||
email_hash = #{emailHash}
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -5,8 +5,8 @@
|
||||
<mapper namespace="com.owrawww.domain.mapper.InquiryMapper">
|
||||
|
||||
<insert id="insert" parameterType="com.owrawww.domain.Inquiry" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO application_table (code, title, name, type, phone, email, comment, depth, in_date, top_code, left_code, sub_gubun, solution_gubun)
|
||||
VALUES (#{code}, #{title}, #{name}, #{type}, #{tel}, #{email}, #{comment}, #{depth}, now(), #{topCode}, #{leftCode}, #{subGubun}, #{solutionGubun})
|
||||
INSERT INTO application_table (code, title, name, type, phone, tel_hash, email, email_hash, comment, depth, in_date, top_code, left_code, sub_gubun, solution_gubun)
|
||||
VALUES (#{code}, #{title}, #{name}, #{type}, #{tel}, #{telHash}, #{email}, #{emailHash}, #{comment}, #{depth}, now(), #{topCode}, #{leftCode}, #{subGubun}, #{solutionGubun})
|
||||
</insert>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -37,3 +37,12 @@ CREATE TABLE IF NOT EXISTS inquiry (
|
||||
-- BCrypt 해시값 사전 생성 필요 시 아래 INSERT 사용
|
||||
-- INSERT INTO users (username, password, role, email, enabled)
|
||||
-- VALUES ('admin', '$2a$10$...', 'ROLE_ADMIN', 'admin@owrawww.com', 1);
|
||||
|
||||
-- =====================================================================
|
||||
-- [마이그레이션] tel_hash / email_hash 컬럼 추가 (솔트 해시 검색용)
|
||||
-- 운영 DB에 아래 ALTER TABLE 를 직접 실행해야 합니다.
|
||||
-- =====================================================================
|
||||
-- ALTER TABLE application_table ADD COLUMN tel_hash VARCHAR(64) AFTER phone;
|
||||
-- ALTER TABLE application_table ADD COLUMN email_hash VARCHAR(64) AFTER email;
|
||||
-- CREATE INDEX idx_app_tel_hash ON application_table(tel_hash);
|
||||
-- CREATE INDEX idx_app_email_hash ON application_table(email_hash);
|
||||
|
||||
100
src/test/java/com/owrawww/controller/HomeControllerTest.java
Normal file
100
src/test/java/com/owrawww/controller/HomeControllerTest.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.owrawww.controller;
|
||||
|
||||
import com.owrawww.domain.Bbs;
|
||||
import com.owrawww.service.BbsService;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.apache.ibatis.transaction.TransactionFactory;
|
||||
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@Import(HomeControllerTest.MybatisTestConfig.class)
|
||||
@WebMvcTest(value = HomeController.class, excludeAutoConfiguration = MybatisAutoConfiguration.class)
|
||||
class HomeControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private BbsService bbsService;
|
||||
|
||||
@Test
|
||||
@DisplayName("GET / - 메인 페이지 정상 응답")
|
||||
void index_returnsIndexView() throws Exception {
|
||||
Bbs bbs = new Bbs();
|
||||
bbs.setId(1L);
|
||||
bbs.setTitle("공지사항");
|
||||
|
||||
given(bbsService.getPreview(anyInt(), anyInt(), anyInt(), anyInt()))
|
||||
.willReturn(List.of(bbs));
|
||||
|
||||
mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("index"))
|
||||
.andExpect(model().attributeExists("progressList"))
|
||||
.andExpect(model().attributeExists("pressList"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /sitemap - 사이트맵 뷰 반환")
|
||||
void sitemap_returnsSitemapView() throws Exception {
|
||||
mockMvc.perform(get("/sitemap"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("sitemap"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /company/intro - 회사소개 뷰 반환")
|
||||
void companyIntro_returnsView() throws Exception {
|
||||
mockMvc.perform(get("/company/intro"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("sub01_01"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /business/intro - 사업소개 뷰 반환")
|
||||
void businessIntro_returnsView() throws Exception {
|
||||
mockMvc.perform(get("/business/intro"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("sub02_01"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /company/map - 오시는길 뷰 반환")
|
||||
void companyMap_returnsView() throws Exception {
|
||||
mockMvc.perform(get("/company/map"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("sub01_05"));
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class MybatisTestConfig {
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory() throws Exception {
|
||||
TransactionFactory tf = new SpringManagedTransactionFactory();
|
||||
javax.sql.DataSource ds = Mockito.mock(javax.sql.DataSource.class);
|
||||
Environment env = new Environment("test", tf, ds);
|
||||
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration(env);
|
||||
SqlSessionFactory factory = Mockito.mock(SqlSessionFactory.class);
|
||||
Mockito.lenient().when(factory.getConfiguration()).thenReturn(conf);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/test/java/com/owrawww/controller/InquiryControllerTest.java
Normal file
138
src/test/java/com/owrawww/controller/InquiryControllerTest.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.owrawww.controller;
|
||||
|
||||
import com.owrawww.service.InquiryService;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.apache.ibatis.transaction.TransactionFactory;
|
||||
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@Import(InquiryControllerTest.MybatisTestConfig.class)
|
||||
@WebMvcTest(value = InquiryController.class, excludeAutoConfiguration = MybatisAutoConfiguration.class)
|
||||
class InquiryControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private InquiryService inquiryService;
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /sub04_02 - 문의 폼 페이지 정상 응답")
|
||||
void inquiryForm_returnsView() throws Exception {
|
||||
mockMvc.perform(get("/sub04_02"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("sub04_02"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /sub04_02 - 유효한 입력 시 성공 응답")
|
||||
void inquirySubmit_validInput_returnsSuccess() throws Exception {
|
||||
given(inquiryService.submit(any())).willReturn(true);
|
||||
|
||||
mockMvc.perform(post("/sub04_02")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("name", "홍길동")
|
||||
.param("email", "hong@example.com")
|
||||
.param("tel", "010-1234-5678")
|
||||
.param("dept", "개발팀")
|
||||
.param("title", "도입 문의")
|
||||
.param("content", "서비스 도입 관련 문의드립니다.")
|
||||
.param("solutionGubun", "PG"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /sub04_02 - 이름 누락 시 400 Bad Request")
|
||||
void inquirySubmit_missingName_returnsBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/sub04_02")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("email", "hong@example.com")
|
||||
.param("tel", "010-1234-5678")
|
||||
.param("dept", "개발팀")
|
||||
.param("title", "도입 문의")
|
||||
.param("content", "서비스 도입 관련 문의드립니다."))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.success").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /sub04_02 - 이메일 형식 오류 시 400 Bad Request")
|
||||
void inquirySubmit_invalidEmail_returnsBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/sub04_02")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("name", "홍길동")
|
||||
.param("email", "not-an-email")
|
||||
.param("tel", "010-1234-5678")
|
||||
.param("dept", "개발팀")
|
||||
.param("title", "도입 문의")
|
||||
.param("content", "내용"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.success").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /sub04_02 - 연락처 형식 오류 시 400 Bad Request")
|
||||
void inquirySubmit_invalidTel_returnsBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/sub04_02")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("name", "홍길동")
|
||||
.param("email", "hong@example.com")
|
||||
.param("tel", "invalid!")
|
||||
.param("dept", "개발팀")
|
||||
.param("title", "도입 문의")
|
||||
.param("content", "내용"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.success").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /sub04_02 - 서비스 저장 실패 시 success false 반환")
|
||||
void inquirySubmit_serviceFailure_returnsFailure() throws Exception {
|
||||
given(inquiryService.submit(any())).willReturn(false);
|
||||
|
||||
mockMvc.perform(post("/sub04_02")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("name", "홍길동")
|
||||
.param("email", "hong@example.com")
|
||||
.param("tel", "010-1234-5678")
|
||||
.param("dept", "개발팀")
|
||||
.param("title", "도입 문의")
|
||||
.param("content", "서비스 도입 관련 문의드립니다.")
|
||||
.param("solutionGubun", "PG"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.message").exists());
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class MybatisTestConfig {
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory() throws Exception {
|
||||
TransactionFactory tf = new SpringManagedTransactionFactory();
|
||||
javax.sql.DataSource ds = Mockito.mock(javax.sql.DataSource.class);
|
||||
Environment env = new Environment("test", tf, ds);
|
||||
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration(env);
|
||||
SqlSessionFactory factory = Mockito.mock(SqlSessionFactory.class);
|
||||
Mockito.lenient().when(factory.getConfiguration()).thenReturn(conf);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/test/java/com/owrawww/service/BbsServiceTest.java
Normal file
103
src/test/java/com/owrawww/service/BbsServiceTest.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.owrawww.service;
|
||||
|
||||
import com.owrawww.domain.Bbs;
|
||||
import com.owrawww.domain.mapper.BbsMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BbsServiceTest {
|
||||
|
||||
@Mock
|
||||
private BbsMapper bbsMapper;
|
||||
|
||||
@InjectMocks
|
||||
private BbsService bbsService;
|
||||
|
||||
private Bbs sampleBbs;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
sampleBbs = new Bbs();
|
||||
sampleBbs.setId(1L);
|
||||
sampleBbs.setTitle("테스트 제목");
|
||||
sampleBbs.setName("작성자");
|
||||
sampleBbs.setBody("내용입니다.");
|
||||
sampleBbs.setViewCnt(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getList - 정상적으로 목록과 페이징 정보를 반환한다")
|
||||
void getList_returnsListAndPagingInfo() {
|
||||
given(bbsMapper.selectList(any())).willReturn(List.of(sampleBbs, sampleBbs));
|
||||
given(bbsMapper.selectCount(any())).willReturn(2);
|
||||
|
||||
Map<String, Object> result = bbsService.getList(1, "all", "", 1, 1, 1);
|
||||
|
||||
assertThat(result.get("list")).isEqualTo(List.of(sampleBbs, sampleBbs));
|
||||
assertThat(result.get("totalCount")).isEqualTo(2);
|
||||
assertThat(result.get("currentPage")).isEqualTo(1);
|
||||
assertThat(result.get("totalPages")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getList - 페이지 번호가 0 이하이면 1로 보정된다")
|
||||
void getList_pageUnderOne_normalizedToOne() {
|
||||
given(bbsMapper.selectList(any())).willReturn(List.of());
|
||||
given(bbsMapper.selectCount(any())).willReturn(0);
|
||||
|
||||
Map<String, Object> result = bbsService.getList(0, "all", "", 1, 1, 1);
|
||||
|
||||
assertThat(result.get("currentPage")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getList - totalCount가 0이면 totalPages는 1이다")
|
||||
void getList_emptyResult_totalPagesIsOne() {
|
||||
given(bbsMapper.selectList(any())).willReturn(List.of());
|
||||
given(bbsMapper.selectCount(any())).willReturn(0);
|
||||
|
||||
Map<String, Object> result = bbsService.getList(1, "all", "", 1, 1, 1);
|
||||
|
||||
assertThat(result.get("totalPages")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getDetail - 조회수 증가 후 게시글과 이전/다음 정보를 반환한다")
|
||||
void getDetail_incrementsViewCntAndReturnsDetail() {
|
||||
given(bbsMapper.selectById(any())).willReturn(sampleBbs);
|
||||
given(bbsMapper.selectPrev(any())).willReturn(null);
|
||||
given(bbsMapper.selectNext(any())).willReturn(null);
|
||||
|
||||
Map<String, Object> result = bbsService.getDetail(1L, 1, 1, 1, 1, "all", "");
|
||||
|
||||
verify(bbsMapper).incrementViewCnt(1L);
|
||||
assertThat(result.get("bbs")).isEqualTo(sampleBbs);
|
||||
assertThat(result.get("prev")).isNull();
|
||||
assertThat(result.get("next")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getPreview - limit 개수만큼 미리보기 목록을 반환한다")
|
||||
void getPreview_returnsLimitedList() {
|
||||
given(bbsMapper.selectPreview(any())).willReturn(List.of(sampleBbs));
|
||||
|
||||
List<Bbs> result = bbsService.getPreview(1, 2, 1, 5);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getTitle()).isEqualTo("테스트 제목");
|
||||
}
|
||||
}
|
||||
87
src/test/java/com/owrawww/service/InquiryServiceTest.java
Normal file
87
src/test/java/com/owrawww/service/InquiryServiceTest.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package com.owrawww.service;
|
||||
|
||||
import com.owrawww.domain.Inquiry;
|
||||
import com.owrawww.domain.mapper.InquiryMapper;
|
||||
import com.owrawww.util.AesUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InquiryServiceTest {
|
||||
|
||||
@Mock
|
||||
private InquiryMapper inquiryMapper;
|
||||
|
||||
@Mock
|
||||
private AesUtil aesUtil;
|
||||
|
||||
@InjectMocks
|
||||
private InquiryService inquiryService;
|
||||
|
||||
private Inquiry inquiry;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
inquiry = new Inquiry();
|
||||
inquiry.setName("홍길동");
|
||||
inquiry.setEmail("hong@example.com");
|
||||
inquiry.setTel("010-1234-5678");
|
||||
inquiry.setTitle("도입 문의");
|
||||
inquiry.setContent("서비스 도입 관련 문의드립니다.");
|
||||
inquiry.setSolutionGubun("PG");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("submit - 정상 입력 시 저장에 성공하고 true를 반환한다")
|
||||
void submit_success() {
|
||||
given(aesUtil.encrypt(inquiry.getTel())).willReturn("ENCRYPTED_TEL");
|
||||
given(aesUtil.encrypt(inquiry.getEmail())).willReturn("ENCRYPTED_EMAIL");
|
||||
given(aesUtil.hash(inquiry.getTel())).willReturn("HASH_TEL");
|
||||
given(aesUtil.hash(inquiry.getEmail())).willReturn("HASH_EMAIL");
|
||||
given(inquiryMapper.insert(any(Inquiry.class))).willReturn(1);
|
||||
|
||||
boolean result = inquiryService.submit(inquiry);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
verify(inquiryMapper).insert(any(Inquiry.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("submit - DB 저장 실패 시 false를 반환한다")
|
||||
void submit_dbFails_returnsFalse() {
|
||||
given(aesUtil.encrypt(any())).willReturn("ENCRYPTED");
|
||||
given(aesUtil.hash(any())).willReturn("HASHED");
|
||||
given(inquiryMapper.insert(any(Inquiry.class))).willReturn(0);
|
||||
|
||||
boolean result = inquiryService.submit(inquiry);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("submit - 전화번호와 이메일이 암호화 및 해시화되어 저장된다")
|
||||
void submit_encryptsAndHashesSensitiveFields() {
|
||||
given(aesUtil.encrypt("010-1234-5678")).willReturn("ENC_TEL");
|
||||
given(aesUtil.encrypt("hong@example.com")).willReturn("ENC_EMAIL");
|
||||
given(aesUtil.hash("010-1234-5678")).willReturn("HASH_TEL");
|
||||
given(aesUtil.hash("hong@example.com")).willReturn("HASH_EMAIL");
|
||||
given(inquiryMapper.insert(any(Inquiry.class))).willReturn(1);
|
||||
|
||||
inquiryService.submit(inquiry);
|
||||
|
||||
verify(aesUtil).encrypt("010-1234-5678");
|
||||
verify(aesUtil).encrypt("hong@example.com");
|
||||
verify(aesUtil).hash("010-1234-5678");
|
||||
verify(aesUtil).hash("hong@example.com");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user