Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ out/

# 환경변수 파일
.env
setup-env.ps1
62 changes: 57 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '4.0.5'
id 'io.spring.dependency-management' version '1.1.7'
id 'jacoco'
}

group = 'com.sparta'
Expand All @@ -13,6 +14,10 @@ java {
}
}

jacoco {
toolVersion = "0.8.11"
}

repositories {
mavenCentral()
}
Expand All @@ -34,16 +39,16 @@ dependencies {

// PostgreSQL 드라이버
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'com.h2database:h2'

// Hibernate Spatial (PostGIS 위치 데이터를 Java 객체로 다루기 위함)
implementation 'org.hibernate.orm:hibernate-spatial'

//JUnit 5, Mockito, AssertJ
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.11.0'

// 스프링 시큐리티 테스트
testImplementation 'org.springframework.security:spring-security-test'
// MockMvc 및 Test Autoconfigure를 명시적으로 추가
testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure'

// Validation 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand All @@ -60,14 +65,61 @@ dependencies {

tasks.named('test') {
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn test
reports {
html.required = true
xml.required = true
csv.required = false
}

afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"**/dto/**",
"**/config/**",
"**/security/**",
"**/*Application*",
"**/entity/**"
])
}))
}
}

jacocoTestCoverageVerification {
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"**/dto/**",
"**/config/**",
"**/security/**",
"**/*Application*",
"**/entity/**",
"**/exception/**",
"**/handler/**",
"**/ai/service/AiService*"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

AiService는 프로젝트의 핵심 기능인 AI 연동 로직을 담당하는 서비스입니다. 테스트 커버리지 검증 대상에서 제외할 경우, 핵심 비즈니스 로직의 테스트 누락을 파악하기 어려워집니다. 해당 제외 설정을 제거하여 커버리지 측정에 포함시키는 것을 권장합니다.

                    "**/handler/**"

])
}))
}
violationRules {
rule {
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.70
}
}
}
}

// 빌드 시 명확한 파일명 지정을 위한 설정
bootJar {
archiveFileName = 'app.jar'
}

// plain jar 생성 방지
jar {
enabled = false
}
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ spring:
name: delivhub
profiles:
active: dev
config:
import: optional:file:.env[.properties]

data:
redis:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.sparta.delivhub;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@Disabled("DB 연결 환경 구축 전까지 컨텍스트 로드 테스트 제외")
@SpringBootTest
class DelivhubApplicationTests {

Expand Down
54 changes: 54 additions & 0 deletions src/test/java/com/sparta/delivhub/common/BaseControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.sparta.delivhub.common;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sparta.delivhub.domain.user.entity.User;
import com.sparta.delivhub.domain.user.entity.UserRole;
import com.sparta.delivhub.security.UserDetailsImpl;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public abstract class BaseControllerTest {

protected MockMvc mockMvc;

@Autowired
protected WebApplicationContext context;

// 스프링이 못 찾으면 우리가 직접 생성 (안전장치)
protected ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());

@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}

protected void mockUserSetup(String username, UserRole role) {
User user = User.builder()
.username(username)
.userRole(role)
.build();

UserDetailsImpl userDetails = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.sparta.delivhub.common.util;

import com.sparta.delivhub.common.dto.BusinessException;
import com.sparta.delivhub.common.dto.ErrorCode;
import com.sparta.delivhub.domain.user.entity.User;
import com.sparta.delivhub.domain.user.entity.UserRole;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class AuthorizationUtilsTest {

@Test
@DisplayName("유틸리티 클래스 생성자 호출 시 예외 발생")
void constructor_Test() throws NoSuchMethodException {
Constructor<AuthorizationUtils> constructor = AuthorizationUtils.class.getDeclaredConstructor();
constructor.setAccessible(true);
assertThatThrownBy(constructor::newInstance)
.isInstanceOf(InvocationTargetException.class)
.hasCauseInstanceOf(IllegalStateException.class);
}

@Test
@DisplayName("OWNER/ADMIN 권한 체크 - CUSTOMER는 거부됨")
void checkOwnerOrAdminPermission_Customer_Fail() {
User user = mock(User.class);
when(user.getUserRole()).thenReturn(UserRole.CUSTOMER);

assertThatThrownBy(() -> AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(ErrorCode.ACCESS_DENIED.getMessage());
}

@Test
@DisplayName("OWNER/ADMIN 권한 체크 - 타인 소유 OWNER는 거부됨")
void checkOwnerOrAdminPermission_OtherOwner_Fail() {
User user = mock(User.class);
when(user.getUserRole()).thenReturn(UserRole.OWNER);
when(user.getUsername()).thenReturn("other");

assertThatThrownBy(() -> AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(ErrorCode.NOT_STORE_OWNER.getMessage());
}

@Test
@DisplayName("OWNER/ADMIN 권한 체크 - 본인 소유 OWNER는 허용")
void checkOwnerOrAdminPermission_MyOwner_Success() {
User user = mock(User.class);
when(user.getUserRole()).thenReturn(UserRole.OWNER);
when(user.getUsername()).thenReturn("owner");

AuthorizationUtils.checkOwnerOrAdminPermission(user, "owner");
}

@Test
@DisplayName("OWNER/ADMIN 권한 체크 - MANAGER/MASTER는 허용")
void checkOwnerOrAdminPermission_Admin_Success() {
User manager = mock(User.class);
when(manager.getUserRole()).thenReturn(UserRole.MANAGER);
AuthorizationUtils.checkOwnerOrAdminPermission(manager, "any");

User master = mock(User.class);
when(master.getUserRole()).thenReturn(UserRole.MASTER);
AuthorizationUtils.checkOwnerOrAdminPermission(master, "any");
}

@Test
@DisplayName("ADMIN 권한 체크 - MANAGER/MASTER 성공")
void checkAdminPermission_Success() {
User manager = mock(User.class);
when(manager.getUserRole()).thenReturn(UserRole.MANAGER);
AuthorizationUtils.checkAdminPermission(manager);

User master = mock(User.class);
when(master.getUserRole()).thenReturn(UserRole.MASTER);
AuthorizationUtils.checkAdminPermission(master);
}

@Test
@DisplayName("ADMIN 권한 체크 - 일반 유저는 실패")
void checkAdminPermission_Fail() {
User customer = mock(User.class);
when(customer.getUserRole()).thenReturn(UserRole.CUSTOMER);
assertThatThrownBy(() -> AuthorizationUtils.checkAdminPermission(customer))
.isInstanceOf(BusinessException.class);

User owner = mock(User.class);
when(owner.getUserRole()).thenReturn(UserRole.OWNER);
assertThatThrownBy(() -> AuthorizationUtils.checkAdminPermission(owner))
.isInstanceOf(BusinessException.class);
}
}
Loading
Loading