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
4 changes: 0 additions & 4 deletions .github/workflows/cd-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ on:
branches:
- 'develop'
- 'main'
pull_request:
branches:
- 'develop'
- 'main'

permissions:
contents: read
Expand Down
62 changes: 44 additions & 18 deletions .github/workflows/ci-workflow.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,64 @@
name: CI with Gradle
name: CI with Gradle & Sonar

on:
push:
branches:
- 'main'
- 'develop'
pull_request:
branches:
- 'main'
- 'develop'

permissions:
contents: read
pull-requests: read

jobs:
build:
build-and-analyze:
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: 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@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: wrapper
cache-read-only: false

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew

- name: 👏🏻 grant execute permission for gradlew
run: chmod +x gradlew
# working-directory: ./U2E
- name: PR Analysis with SonarQube
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PR 데코레이션용(자동 제공, Secrets에 별도 추가 불필요)
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SonarCloud/SonarQube 인증 토큰(Secrets 필요)
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

- name: 🐘 build with Gradle (without test)
run: ./gradlew clean build -x test --stacktrace
# working-directory: ./U2E
- 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
127 changes: 118 additions & 9 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 '5.0.0.4638'
}

group = 'Konkuk'
Expand All @@ -23,6 +25,17 @@ repositories {
mavenCentral()
}

sonarqube {
properties {
property "sonar.projectKey", "Us2Earth_U2E-Server"
property "sonar.host.url", "https://sonarcloud.io"
property "sonar.organization", "us2earth"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.coverage.jacoco.xmlReportPaths",
layout.buildDirectory.file("reports/test/jacocoTestReport.xml").get().asFile.absolutePath
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -34,24 +47,120 @@ 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'

// WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.core:jackson-annotations'
implementation 'com.fasterxml.jackson.core:jackson-core'

// 헬스 체크 api 를 사용하기 위한 Actuator 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-actuator'
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'

// WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.core:jackson-annotations'
implementation 'com.fasterxml.jackson.core:jackson-core'

// 헬스 체크 api 를 사용하기 위한 Actuator 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

jacoco {
toolVersion = "0.8.12"
}

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')
}

tasks.named('sonarqube') {
dependsOn tasks.jacocoTestReport
}
7 changes: 3 additions & 4 deletions src/main/java/Konkuk/U2E/domain/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
2 changes: 2 additions & 0 deletions src/test/java/Konkuk/U2E/U2EApplicationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 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
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));
}
}
Loading
Loading