From 8a2952b683a493050d510a4792f2fbfab1a6d8a6 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 01:49:50 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[test]=20ci=EC=8B=9C=EC=97=90=20jacoco,?= =?UTF-8?q?=20sonarqube=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 51 +++++++--- build.gradle | 17 ++++ jacoco.gradle | 92 +++++++++++++++++++ .../Konkuk/U2E/domain/user/domain/User.java | 7 +- .../user/controller/UserControllerTest.java | 67 ++++++++++++++ .../domain/user/service/UserServiceTest.java | 72 +++++++++++++++ 6 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 jacoco.gradle create mode 100644 src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index e60baa7..8e6c696 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,4 +1,4 @@ -name: CI with Gradle +name: CI with Gradle & Sonar on: push: @@ -12,27 +12,50 @@ on: permissions: contents: read + pull-requests: read jobs: - build: + build-and-analyze: + name: Build, Test, JaCoCo, Sonar runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Sonar 분석 정확도 향상 및 PR 데코레이션을 위해 전체 이력 필요 + - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - java-version: '21' distribution: 'temurin' - server-id: github - settings-path: ${{ github.workspace }} # location for the settings.xml file + java-version: '21' + cache: 'gradle' - name: Setup Gradle - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + uses: gradle/action s/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + # working-directory: ./U2E # 서브모듈/하위 디렉터리일 경우 주석 해제 후 경로 지정 - - name: 👏🏻 grant execute permission for gradlew - run: chmod +x gradlew -# working-directory: ./U2E + # 테스트 실행 + JaCoCo 리포트 + Sonar 분석 + # - jacocoTestReport는 xml 리포트를 생성해야 하며, build.gradle 설정 필요(아래 3번 참고) + - name: Build, Test, Coverage, Sonar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PR 데코레이션용(자동 제공, Secrets에 별도 추가 불필요) + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SonarCloud/SonarQube 인증 토큰(Secrets 필요) + # SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} # (자체 SonarQube 서버 사용 시 필요) + run: | + ./gradlew --version + ./gradlew clean test jacocoTestReport sonarqube --info --stacktrace + # working-directory: ./U2E - - name: 🐘 build with Gradle (without test) - run: ./gradlew clean build -x test --stacktrace -# working-directory: ./U2E \ No newline at end of file + - name: Upload JaCoCo HTML report artifact (optional) + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-html + path: build/reports/test/jacocoTestReportHtml + if-no-files-found: ignore + # working-directory: ./U2E \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4007f90..378a7c7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' + id 'org.sonarqube' version '3.3' } group = 'Konkuk' @@ -23,6 +25,16 @@ repositories { mavenCentral() } +sonarqube { + properties { + property "sonar.projectKey", "Us2Earth_U2E-Server" + property "sonar.host.url", "https://sonarcloud.io" + property "sonar.organization", "Us2Earth" + property "sonar.java.binaries", "build/classes/java/main" + property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/test/jacocoTestReport/jacocoTestReport.xml" + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -41,6 +53,11 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0' + testImplementation 'org.mockito:mockito-core:5.12.0' + testImplementation 'org.assertj:assertj-core:3.26.0' } tasks.named('test') { diff --git a/jacoco.gradle b/jacoco.gradle new file mode 100644 index 0000000..a36e3d4 --- /dev/null +++ b/jacoco.gradle @@ -0,0 +1,92 @@ +jacoco { + toolVersion = "0.8.12" +} + +lombok.addLombokGeneratedAnnotation = true + +tasks.named('test') { + useJUnitPlatform() + finalizedBy tasks.jacocoTestReport +} + +tasks.named('jacocoTestReport', JacocoReport) { + dependsOn(tasks.test) + + def mainClasses = files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ) + } + + additionalSourceDirs.from files(sourceSets.main.allSource.srcDirs) + sourceDirectories.from files(sourceSets.main.allSource.srcDirs) + classDirectories.setFrom(mainClasses) + executionData.from fileTree(project.rootDir) { + include "**/build/jacoco/*.exec" + } + + reports { + html.required.set(true) // 로컬 확인용 HTML 리포트 생성 + html.outputLocation.set(layout.buildDirectory.dir("reports/test/jacocoTestReportHtml")) + + xml.required.set(true) // SonarCloud 업로드용 XML 리포트 생성 + xml.outputLocation.set(layout.buildDirectory.file("reports/test/jacocoTestReport.xml")) + + csv.required.set(false) // CSV 비활성화 + } +} + +tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { + dependsOn(tasks.test) + + classDirectories.setFrom( + files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ) + } + ) + + violationRules { + rule { + element = 'CLASS' + limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.00 + } + } + } +} + +tasks.named('check') { + dependsOn tasks.named('jacocoTestCoverageVerification') +} + +apply from: "$rootDir/jacoco.gradle" \ No newline at end of file diff --git a/src/main/java/Konkuk/U2E/domain/user/domain/User.java b/src/main/java/Konkuk/U2E/domain/user/domain/User.java index cf26514..7f03799 100644 --- a/src/main/java/Konkuk/U2E/domain/user/domain/User.java +++ b/src/main/java/Konkuk/U2E/domain/user/domain/User.java @@ -2,14 +2,13 @@ import Konkuk.U2E.global.entity.BaseEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "users") @Getter +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity { diff --git a/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java b/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java new file mode 100644 index 0000000..fa2e864 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java @@ -0,0 +1,67 @@ +package Konkuk.U2E.domain.user.controller; + +import Konkuk.U2E.domain.user.dto.request.PostUserLoginRequest; +import Konkuk.U2E.domain.user.dto.response.PostUserLoginResponse; +import Konkuk.U2E.domain.user.service.UserService; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.MediaType; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class UserControllerTest { + + private UserService userService; // mock + private UserController userController; + + @BeforeEach + void setUp() { + userService = Mockito.mock(UserService.class); + userController = new UserController(userService); + RestAssuredMockMvc.standaloneSetup(userController); + } + + @AfterEach + void tearDown() { + RestAssuredMockMvc.reset(); + } + + @Test + @DisplayName("POST /user/login 성공 - BaseResponse 포맷(success, code, message, data) 검증") + void login_success() { + // given + long userId = 1L; + String token = "mock-jwt-token"; + when(userService.signupAndLogin(any(PostUserLoginRequest.class))) + .thenReturn(PostUserLoginResponse.of(userId, token)); + + // when & then + RestAssuredMockMvc + .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(""" + { + "name": "alice", + "password": "password123" + } + """) + .when() + .post("/user/login") + .then() + .statusCode(200) + // BaseResponse 공통 필드 + .body("success", Matchers.is(true)) + .body("code", Matchers.notNullValue()) + .body("message", Matchers.notNullValue()) + // data 내부 필드 + .body("data", Matchers.notNullValue()) + .body("data.userId", Matchers.equalTo((int) userId)) + .body("data.token", Matchers.equalTo(token)); + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java b/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..7c8c535 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java @@ -0,0 +1,72 @@ +package Konkuk.U2E.domain.user.service; + +import Konkuk.U2E.domain.user.domain.User; +import Konkuk.U2E.domain.user.dto.request.PostUserLoginRequest; +import Konkuk.U2E.domain.user.dto.response.PostUserLoginResponse; +import Konkuk.U2E.domain.user.exception.DuplicateUserException; +import Konkuk.U2E.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static Konkuk.U2E.global.response.status.BaseExceptionResponseStatus.DUPLICATE_USER; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class UserServiceTest { + + @Test + @DisplayName("회원 미존재 시 생성 후 토큰 발급") + void signupAndLogin_createsAndIssuesToken() { + // given + UserRepository repo = mock(UserRepository.class); + JwtUtil jwt = mock(JwtUtil.class); + UserService sut = new UserService(repo, jwt); + + PostUserLoginRequest req = new PostUserLoginRequest("alice", "password123"); + + when(repo.findUserByName("alice")).thenReturn(null); + User saved = User.builder() + .userId(1L) + .name("alice") + .password("password123") + .build(); + when(repo.save(any(User.class))).thenReturn(saved); + when(jwt.generateAccessToken("alice")).thenReturn("mock-jwt-token"); + + // when + PostUserLoginResponse res = sut.signupAndLogin(req); + + // then + assertThat(res).isNotNull(); + assertThat(res.userId()).isEqualTo(1L); + assertThat(res.token()).isEqualTo("mock-jwt-token"); + + verify(repo).findUserByName("alice"); + verify(repo).save(any(User.class)); + verify(jwt).generateAccessToken("alice"); + } + + @Test + @DisplayName("중복 사용자 존재 시 DuplicateUserException 발생") + void signupAndLogin_duplicateUser_throws() { + // given + UserRepository repo = mock(UserRepository.class); + JwtUtil jwt = mock(JwtUtil.class); + UserService sut = new UserService(repo, jwt); + + PostUserLoginRequest req = new PostUserLoginRequest("alice", "pw"); + + when(repo.findUserByName("alice")) + .thenReturn(User.builder().userId(99L).name("alice").password("pw").build()); + + // when & then + assertThatThrownBy(() -> sut.signupAndLogin(req)) + .isInstanceOf(DuplicateUserException.class) + .hasFieldOrPropertyWithValue("status", DUPLICATE_USER); + + verify(repo).findUserByName("alice"); + verify(repo, never()).save(any(User.class)); + verifyNoInteractions(jwt); + } +} \ No newline at end of file From f8158a988d24456cdf8af8f5ea4fb132239d8b5a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 01:52:28 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[test]=20ci,=20cd=20=EC=97=AD=ED=95=A0=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-workflow.yml | 4 ---- .github/workflows/ci-workflow.yml | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml index 67768c3..fccde1f 100644 --- a/.github/workflows/cd-workflow.yml +++ b/.github/workflows/cd-workflow.yml @@ -5,10 +5,6 @@ on: branches: - 'develop' - 'main' - pull_request: - branches: - - 'develop' - - 'main' permissions: contents: read diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 8e6c696..5e7b15e 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,10 +1,6 @@ name: CI with Gradle & Sonar on: - push: - branches: - - 'main' - - 'develop' pull_request: branches: - 'main' @@ -33,7 +29,7 @@ jobs: cache: 'gradle' - name: Setup Gradle - uses: gradle/action s/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for gradlew run: chmod +x ./gradlew From 0342ebca679be2e70fdf58abc2468adc5eb98818 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:00:06 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=98=88=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Konkuk/U2E/U2EApplicationTests.java | 2 ++ .../user/controller/UserControllerTest.java | 2 ++ .../domain/user/service/UserServiceTest.java | 4 ++- src/test/resources/application-test.yml | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/application-test.yml diff --git a/src/test/java/Konkuk/U2E/U2EApplicationTests.java b/src/test/java/Konkuk/U2E/U2EApplicationTests.java index 17c216a..e385aae 100644 --- a/src/test/java/Konkuk/U2E/U2EApplicationTests.java +++ b/src/test/java/Konkuk/U2E/U2EApplicationTests.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest class U2EApplicationTests { diff --git a/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java b/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java index fa2e864..defd0a6 100644 --- a/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java +++ b/src/test/java/Konkuk/U2E/domain/user/controller/UserControllerTest.java @@ -11,10 +11,12 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +@ActiveProfiles("test") class UserControllerTest { private UserService userService; // mock diff --git a/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java b/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java index 7c8c535..02ed37a 100644 --- a/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java +++ b/src/test/java/Konkuk/U2E/domain/user/service/UserServiceTest.java @@ -7,12 +7,14 @@ import Konkuk.U2E.domain.user.repository.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; import static Konkuk.U2E.global.response.status.BaseExceptionResponseStatus.DUPLICATE_USER; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +@ActiveProfiles("test") class UserServiceTest { @Test @@ -63,7 +65,7 @@ void signupAndLogin_duplicateUser_throws() { // when & then assertThatThrownBy(() -> sut.signupAndLogin(req)) .isInstanceOf(DuplicateUserException.class) - .hasFieldOrPropertyWithValue("status", DUPLICATE_USER); + .hasFieldOrPropertyWithValue("message", DUPLICATE_USER.getMessage()); verify(repo).findUserByName("alice"); verify(repo, never()).save(any(User.class)); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..dfb0959 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,30 @@ +spring: + application: + name: U2E + +jwt: + secret: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz + expiration: 3600000 +--- +spring: + datasource: + url: jdbc:h2:mem:U2EApplication;MODE=MySQL; + username: sa + password: + driver-class-name: org.h2.Driver + + h2: + console: + enabled: true + path: /h2-console + + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect + + From fa0cd0a5efda778e0df370c1ec326ec8ea987d31 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:06:35 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[test]=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 378a7c7..d4bc7dc 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' id 'jacoco' - id 'org.sonarqube' version '3.3' + id 'org.sonarqube' version '7.0.1.6134' } group = 'Konkuk' @@ -30,8 +30,10 @@ sonarqube { property "sonar.projectKey", "Us2Earth_U2E-Server" property "sonar.host.url", "https://sonarcloud.io" property "sonar.organization", "Us2Earth" - property "sonar.java.binaries", "build/classes/java/main" - property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/test/jacocoTestReport/jacocoTestReport.xml" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", + layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml") + .get().asFile.absolutePath } } From b7d5c4475de11c15ffcc7f4a1674ec73f0e30be9 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:12:28 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[test]=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d4bc7dc..5c260e2 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ sonarqube { property "sonar.organization", "Us2Earth" property "sonar.java.coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", - layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml") + layout.buildDirectory.file("reports/test/jacocoTestReport.xml") .get().asFile.absolutePath } } From c70329f42a0303046e790eee59be47cfe72c86dc Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:43:42 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[test]=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 96 ++++++++++++++++++++++++++++++++++++++++++++++++--- jacoco.gradle | 92 ------------------------------------------------ 2 files changed, 92 insertions(+), 96 deletions(-) delete mode 100644 jacoco.gradle diff --git a/build.gradle b/build.gradle index 5c260e2..4dcf614 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' id 'jacoco' - id 'org.sonarqube' version '7.0.1.6134' + id 'org.sonarqube' version '6.0.1.4882' } group = 'Konkuk' @@ -32,8 +32,7 @@ sonarqube { property "sonar.organization", "Us2Earth" property "sonar.java.coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", - layout.buildDirectory.file("reports/test/jacocoTestReport.xml") - .get().asFile.absolutePath + layout.buildDirectory.file("reports/test/jacocoTestReport.xml").get().asFile.absolutePath } } @@ -48,10 +47,11 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // jwt + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' @@ -62,6 +62,94 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.26.0' } +jacoco { + toolVersion = "0.8.12" +} + +lombok.addLombokGeneratedAnnotation = true + tasks.named('test') { useJUnitPlatform() + finalizedBy tasks.jacocoTestReport +} + +tasks.named('jacocoTestReport', JacocoReport) { + dependsOn(tasks.test) + + def mainClasses = files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ) + } + + additionalSourceDirs.from files(sourceSets.main.allSource.srcDirs) + sourceDirectories.from files(sourceSets.main.allSource.srcDirs) + classDirectories.setFrom(mainClasses) + + executionData.from fileTree(project.rootDir) { + include "**/build/jacoco/*.exec" + } + + reports { + html.required.set(true) + html.outputLocation.set(layout.buildDirectory.dir("reports/test/jacocoTestReportHtml")) + + xml.required.set(true) + xml.outputLocation.set(layout.buildDirectory.file("reports/test/jacocoTestReport.xml")) + + csv.required.set(false) + } +} + +tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { + dependsOn(tasks.test) + + classDirectories.setFrom( + files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ) + } + ) + + violationRules { + rule { + element = 'CLASS' + limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.00 + } + } + } +} + +tasks.named('check') { + dependsOn tasks.named('jacocoTestCoverageVerification') } diff --git a/jacoco.gradle b/jacoco.gradle deleted file mode 100644 index a36e3d4..0000000 --- a/jacoco.gradle +++ /dev/null @@ -1,92 +0,0 @@ -jacoco { - toolVersion = "0.8.12" -} - -lombok.addLombokGeneratedAnnotation = true - -tasks.named('test') { - useJUnitPlatform() - finalizedBy tasks.jacocoTestReport -} - -tasks.named('jacocoTestReport', JacocoReport) { - dependsOn(tasks.test) - - def mainClasses = files(sourceSets.main.output).asFileTree.matching { - exclude( - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ) - } - - additionalSourceDirs.from files(sourceSets.main.allSource.srcDirs) - sourceDirectories.from files(sourceSets.main.allSource.srcDirs) - classDirectories.setFrom(mainClasses) - executionData.from fileTree(project.rootDir) { - include "**/build/jacoco/*.exec" - } - - reports { - html.required.set(true) // 로컬 확인용 HTML 리포트 생성 - html.outputLocation.set(layout.buildDirectory.dir("reports/test/jacocoTestReportHtml")) - - xml.required.set(true) // SonarCloud 업로드용 XML 리포트 생성 - xml.outputLocation.set(layout.buildDirectory.file("reports/test/jacocoTestReport.xml")) - - csv.required.set(false) // CSV 비활성화 - } -} - -tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { - dependsOn(tasks.test) - - classDirectories.setFrom( - files(sourceSets.main.output).asFileTree.matching { - exclude( - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ) - } - ) - - violationRules { - rule { - element = 'CLASS' - limit { - counter = 'INSTRUCTION' - value = 'COVEREDRATIO' - minimum = 0.00 - } - limit { - counter = 'BRANCH' - value = 'COVEREDRATIO' - minimum = 0.00 - } - limit { - counter = 'LINE' - value = 'COVEREDRATIO' - minimum = 0.00 - } - } - } -} - -tasks.named('check') { - dependsOn tasks.named('jacocoTestCoverageVerification') -} - -apply from: "$rootDir/jacoco.gradle" \ No newline at end of file From 9c0ed797ceb0dd083531cbc01ce6ce78a1d57f6e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:48:14 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[test]=20sonarqube=EA=B0=80=20jacoco=20re?= =?UTF-8?q?port=20=EC=83=9D=EC=84=B1=ED=9B=84=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4dcf614..342e666 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' id 'jacoco' - id 'org.sonarqube' version '6.0.1.4882' + id 'org.sonarqube' version '5.0.0.4638' } group = 'Konkuk' @@ -153,3 +153,7 @@ tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { tasks.named('check') { dependsOn tasks.named('jacocoTestCoverageVerification') } + +tasks.named('sonarqube') { + dependsOn tasks.jacocoTestReport +} From a52b6145d3cde564402ebbba9d07e437f11d3a54 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:50:50 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[test]=20jacoco=20lombok=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 342e666..534dea3 100644 --- a/build.gradle +++ b/build.gradle @@ -66,8 +66,6 @@ jacoco { toolVersion = "0.8.12" } -lombok.addLombokGeneratedAnnotation = true - tasks.named('test') { useJUnitPlatform() finalizedBy tasks.jacocoTestReport From c169f53c3a07eaaf4b858619b2fc2ea7bffa52c5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 02:54:35 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[test]=20sonaqube=20project=20key=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 534dea3..4bde5e4 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ sonarqube { properties { property "sonar.projectKey", "Us2Earth_U2E-Server" property "sonar.host.url", "https://sonarcloud.io" - property "sonar.organization", "Us2Earth" + property "sonar.organization", "us2earth" property "sonar.java.coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", layout.buildDirectory.file("reports/test/jacocoTestReport.xml").get().asFile.absolutePath From f3a435e5a6416eca84d0e0ea10b3e5ea6b0fadce Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 03:01:40 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[chore]=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 5e7b15e..71e5a4a 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -35,8 +35,6 @@ jobs: run: chmod +x ./gradlew # working-directory: ./U2E # 서브모듈/하위 디렉터리일 경우 주석 해제 후 경로 지정 - # 테스트 실행 + JaCoCo 리포트 + Sonar 분석 - # - jacocoTestReport는 xml 리포트를 생성해야 하며, build.gradle 설정 필요(아래 3번 참고) - name: Build, Test, Coverage, Sonar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PR 데코레이션용(자동 제공, Secrets에 별도 추가 불필요) From 52621f75af1aa97d3826eab3c71c98507619cfdd Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 03:12:04 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[test]=20ci=EC=8B=9C=EC=97=90=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20+=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 50 +++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 71e5a4a..32c4049 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -2,9 +2,7 @@ name: CI with Gradle & Sonar on: pull_request: - branches: - - 'main' - - 'develop' + branches: [ "main", "develop" ] permissions: contents: read @@ -12,44 +10,58 @@ permissions: jobs: build-and-analyze: - name: Build, Test, JaCoCo, Sonar runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # Sonar 분석 정확도 향상 및 PR 데코레이션을 위해 전체 이력 필요 + fetch-depth: 0 - name: Set up JDK 21 uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: '21' - cache: 'gradle' + cache: gradle - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for gradlew run: chmod +x ./gradlew - # working-directory: ./U2E # 서브모듈/하위 디렉터리일 경우 주석 해제 후 경로 지정 - - name: Build, Test, Coverage, Sonar + # 1) PR 분석(변경분) + - name: PR Analysis (diff only) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + ./gradlew clean test jacocoTestReport \ + sonarqube \ + -Dsonar.login=${SONAR_TOKEN} \ + -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \ + -Dsonar.pullrequest.branch=${{ github.head_ref }} \ + -Dsonar.pullrequest.base=${{ github.base_ref }} \ + --info --stacktrace + + # 2) 전체 분석(브랜치) - 전체 코드 스멜/이슈 확인용 + - name: Full Branch Analysis (entire code) + if: always() env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PR 데코레이션용(자동 제공, Secrets에 별도 추가 불필요) - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SonarCloud/SonarQube 인증 토큰(Secrets 필요) - # SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} # (자체 SonarQube 서버 사용 시 필요) + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | - ./gradlew --version - ./gradlew clean test jacocoTestReport sonarqube --info --stacktrace - # working-directory: ./U2E + # PR 파라미터가 자동 감지되지 않도록, 필요한 값만 명시 + BRANCH_NAME="pr-${{ github.event.pull_request.number }}-full" + ./gradlew sonarqube \ + -Dsonar.login=${SONAR_TOKEN} \ + -Dsonar.branch.name=${BRANCH_NAME} \ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/test/jacocoTestReport.xml \ + --info --stacktrace - - name: Upload JaCoCo HTML report artifact (optional) + - name: Upload JaCoCo HTML (optional) if: always() uses: actions/upload-artifact@v4 with: name: jacoco-html path: build/reports/test/jacocoTestReportHtml - if-no-files-found: ignore - # working-directory: ./U2E \ No newline at end of file + if-no-files-found: ignore \ No newline at end of file From b2e82e512bcc3e0446edb67668ed089d480e0ea3 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 27 Oct 2025 03:19:39 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[test]=20ci=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 49 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 32c4049..42769e1 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -2,7 +2,9 @@ name: CI with Gradle & Sonar on: pull_request: - branches: [ "main", "develop" ] + branches: + - 'main' + - 'develop' permissions: contents: read @@ -11,57 +13,52 @@ permissions: jobs: build-and-analyze: runs-on: ubuntu-latest + steps: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 # Sonar 분석 정확도 향상 및 PR 데코레이션을 위해 전체 이력 필요 - name: Set up JDK 21 uses: actions/setup-java@v4 with: - distribution: temurin + distribution: 'temurin' java-version: '21' - cache: gradle + cache: 'gradle' + + - name: Cache SonarQube packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper + cache-read-only: false - name: Grant execute permission for gradlew run: chmod +x ./gradlew - # 1) PR 분석(변경분) - - name: PR Analysis (diff only) + - name: PR Analysis with SonarQube env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PR 데코레이션용(자동 제공, Secrets에 별도 추가 불필요) + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SonarCloud/SonarQube 인증 토큰(Secrets 필요) run: | - ./gradlew clean test jacocoTestReport \ - sonarqube \ + ./gradlew clean test jacocoTestReport sonarqube \ -Dsonar.login=${SONAR_TOKEN} \ -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \ -Dsonar.pullrequest.branch=${{ github.head_ref }} \ -Dsonar.pullrequest.base=${{ github.base_ref }} \ --info --stacktrace - # 2) 전체 분석(브랜치) - 전체 코드 스멜/이슈 확인용 - - name: Full Branch Analysis (entire code) - if: always() - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - # PR 파라미터가 자동 감지되지 않도록, 필요한 값만 명시 - BRANCH_NAME="pr-${{ github.event.pull_request.number }}-full" - ./gradlew sonarqube \ - -Dsonar.login=${SONAR_TOKEN} \ - -Dsonar.branch.name=${BRANCH_NAME} \ - -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/test/jacocoTestReport.xml \ - --info --stacktrace - - - name: Upload JaCoCo HTML (optional) + - name: Upload JaCoCo HTML report if: always() uses: actions/upload-artifact@v4 with: name: jacoco-html path: build/reports/test/jacocoTestReportHtml - if-no-files-found: ignore \ No newline at end of file + if-no-files-found: ignore