Skip to content
Closed
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
123 changes: 123 additions & 0 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: PR CI (main)

on:
pull_request:
branches: [ "main" ] # main으로 향하는 PR만 검사
types: [ opened, synchronize, reopened, ready_for_review ] # PR 열림/커밋추가/다시열기/드래프트해제 시 실행
paths-ignore:
- '**.md'
- 'docs/**'

jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15 # CI가 너무 오래 걸릴 때 무한 대기 방지 (필요시 늘려도 됨)
strategy:
matrix:
dbmode: [ mysql, postgres ] # DB 의존 로직 호환성 점검을 위해 H2를 두 모드로 테스트
fail-fast: false # 한 모드가 실패해도 나머지 모드는 계속 실행(진단에 유리)

steps:
# 1) 코드 체크아웃
- name: Checkout code
uses: actions/checkout@v4

# 2) JDK 설치 (프로젝트 toolchain=21과 일치)
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle # Gradle 캐시 자동화( wrapper/caches )

# 3) gradlew 실행권한 부여
- name: Grant execute permission for gradlew
run: chmod +x gradlew

# 4) Gradle 래퍼 유효성 체크(선택이지만 문제 파악에 도움)
- name: Validate Gradle wrapper
run: ./gradlew --version

# 5) 업로드 경로 더미 생성 (upload.path 값이 필요한 빈 대비)
- name: Prepare upload dir
run: mkdir -p /tmp/uploads

# 6) 테스트
- name: Run tests (H2 via ENV)
run: ./gradlew clean test --no-daemon --stacktrace --info
env:
# (스프링) test 프로필로 기동 — 실제 파일 없어도 ENV가 모든 값을 오버라이드
SPRING_PROFILES_ACTIVE: test

# (DB) H2 메모리 DB URL — dbmode 매트릭스 값에 따라 MySQL/Postgre 모드 전환
SPRING_DATASOURCE_URL: ${{ matrix.dbmode == 'mysql'
&& 'jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;DB_CLOSE_ON_EXIT=FALSE'
|| 'jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;DB_CLOSE_ON_EXIT=FALSE' }}
SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.h2.Driver
SPRING_DATASOURCE_USERNAME: sa
SPRING_DATASOURCE_PASSWORD: ""

# (JPA) 테스트 중 스키마 자동 생성/삭제 — 외부 스키마 의존 제거
SPRING_JPA_HIBERNATE_DDL_AUTO: create-drop
SPRING_SQL_INIT_MODE: never

# 외부 리소스 자동설정 차단(네트워크 시도 방지)
SPRING_AUTOCONFIGURE_EXCLUDE: >
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration

# 세션 저장소 비활성화
SPRING_SESSION_STORE_TYPE: none

# GCP Secret Manager 무력화
SPRING_CLOUD_GCP_SECRET_MANAGER_ENABLED: "false"
SPRING_CONFIG_IMPORT: ""

# ---- 자리값 더미(컨텍스트 부팅용) ---
# 파일 업로드 경로 (핵심)
UPLOAD_PATH: /tmp/uploads # upload.path와 자동 매핑 (relaxed binding)

# JWT / Mail
JWT_SECRET: dummy
SMTP_USERNAME: [email protected]
SMTP_PASSWORD: dummy

# OAuth2
GOOGLE_CLIENT_ID: dummy
GOOGLE_CLIENT_SECRET: dummy
GOOGLE_REDIRECT_URI: http://localhost/login/oauth2/code/google

# Redis (값만 읽는 코드 대비)
SPRING_DATA_REDIS_HOST: localhost
SPRING_DATA_REDIS_PORT: "6379"
SPRING_DATA_REDIS_USERNAME: default
SPRING_DATA_REDIS_PASSWORD: dummy
SPRING_DATA_REDIS_SSL_ENABLED: "false"

# 도메인 기본값
FRONT_SERVER_DOMAIN: http://localhost:3000
APP_DOMAIN: http://localhost:80

# LangChain/Gemini
GEMINI_API_KEY: "dummy"

# CI 로그 노이즈 감소 — 필요시 조정
SPRING_JPA_SHOW_SQL: "false"
SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL: "false"
LOGGING_LEVEL_ROOT: "warn"
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: "warn"

# 6) 테스트 리포트 업로드 — 실패 시에도 항상 업로드하여 원인 파악
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report-${{ matrix.dbmode }}
path: |
build/reports/tests/test
retention-days: 30
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ replay_pid*

# === Spring Boot 환경설정 파일 ===
application.properties
application-*.properties
application-local.properties
application-test.properties
application-prod.properties
application-prod-aws.properties
application-prod-gcp.properties

# === 기타 ===
HELP.md
Expand Down
157 changes: 157 additions & 0 deletions src/main/resources/application-template.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# ????????????????????????????????????????????????????????????????????????????????????????????
# ??????????????????????? Application Template (DO NOT PUT SECRETS) ???????????????????????
# ?? ?? ????/??? ???? ?????.
# - GCP: ${sm://secret-name} ?? ??
# - AWS: ${ENV_NAME} ????/Secrets Manager ??
# ?????????????????????????????????????????????????????????????????????????????????????????
# ????????????????????????????????????????????????????????????????????????????????????????????

# ---- Profile ----
# ?? ??/?? ? -Dspring.profiles.active=prod-gcp ?? prod-aws ? ??
spring.profiles.active=

# ---- Server ----
server.port=80
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true

# ---- Domain / CORS (???? ?? ???) ----
front-server.domain=${FRONT_SERVER_DOMAIN:https://example-frontend.com}
app.domain=${APP_DOMAIN:https://example-backend.com}

# ---- File Upload / Multipart ----
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

# ---- Cache ----
spring.cache.type=caffeine
spring.cache.cache-names=authority
spring.cache.caffeine.spec=expireAfterWrite=10s

# ---- Logging ?? ----
logging.level.root=info
logging.level.org.hibernate.SQL=info
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=info
logging.level.org.hibernate.type.descriptor.sql=info
logging.level.org.springframework.cloud.openfeign=info
logging.level.org.springframework.security=info
logging.level.org.springframework.web.client.RestTemplate=info
logging.level.org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider=info
logging.level.org.springframework.security.oauth2.core.http.converter=info
spring.cloud.openfeign.client.config.default.logger-level=full

# ---- JPA ?? ----
spring.jpa.open-in-view=false
spring.sql.init.mode=never
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql

# ??????????????????????????????????????????????????????????????????????????????????????????
# ???????????????????????????? ??? ?? ??? ???? ?? ???????????????????????????
# 1) GCP ?? (Secret Manager ?? ?)
# - spring.config.import=sm:// ? ??, ?? ?? ??? sm://? ??
# 2) AWS ?? (ENV/Secrets Manager)
# - ${ENV_NAME} ??? ????, ECS/EC2?? ????? ??
# ?????????????????????????????????????????????????????????????????????????????????????????
# ??????????????????????????????????????????????????????????????????????????????????????????

# [prod-gcp] ?? ==========================================================================
# spring.profiles: prod-gcp ?? ? ???
# ?? ??: -Dspring.profiles.active=prod-gcp
# ==========================================================================================
# spring.config.import=sm:// # GCP Secret Manager ?? ? ??

# spring.cloud.gcp.secretmanager.enabled=true
# google.cloud.storage.bucket=YOUR_GCS_BUCKET
# google.cloud.storage.project-id=YOUR_GCP_PROJECT

# ---- DB ----
#spring.datasource.url=${sm://db-url}
#spring.datasource.username=${sm://db-username}
#spring.datasource.password=${sm://db-password}
#spring.datasource.driver-class-name=org.postgresql.Driver
#spring.datasource.hikari.maximum-pool-size=5

# ---- Redis ----
#spring.data.redis.host=${sm://redis-host}
#spring.data.redis.port=${sm://redis-port}
#spring.data.redis.username=${sm://redis-username}
#spring.data.redis.password=${sm://redis-password}
#spring.data.redis.ssl.enabled=true

# ---- JWT ----
#jwt.expiration=${sm://jwt-expiration}
#jwt.refresh-expiration=${sm://jwt-refresh-expiration}
#jwt.secret=${sm://jwt-secret}

# ---- Mail ----
#spring.mail.host=smtp.gmail.com
#spring.mail.port=587
#spring.mail.username=${sm://smtp-username}
#spring.mail.password=${sm://smtp-password}
#spring.mail.properties.mail.smtp.auth=true
#spring.mail.properties.mail.smtp.starttls.enable=true

# ---- Google OAuth ----
#spring.security.oauth2.client.registration.google.client-id=${sm://google-client-id}
#spring.security.oauth2.client.registration.google.client-secret=${sm://google-secret}
#spring.security.oauth2.client.registration.google.redirect-uri=${sm://google-redirect-uri}
#spring.security.oauth2.client.registration.google.scope=profile,email
#spring.security.oauth2.client.registration.google.client-name=Google

# ---- LangChain/Gemini ----
#langchain4j.google-ai-gemini.chat-model.model-name=${sm://gemini-chat-model-name}
#langchain4j.google-ai-gemini.chat-model.api-key=${sm://gemini-chat-model}
#langchain4j.google-ai-gemini.chat-model.log-requests-and-responses=true
#langchain4j.google-ai-gemini.chat-model.max-output-tokens=500

# [prod-aws] ?? ==========================================================================
# spring.profiles: prod-aws ?? ? ???
# ?? ??: -Dspring.profiles.active=prod-aws
# ==========================================================================================

# ---- DB ----
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=5

# ---- Redis ----
spring.data.redis.host=${REDIS_HOST}
spring.data.redis.port=${REDIS_PORT}
spring.data.redis.username=${REDIS_USERNAME:default}
spring.data.redis.password=${REDIS_PASSWORD}
spring.data.redis.ssl.enabled=true

# ---- JWT ----
jwt.secret=${JWT_SECRET}
jwt.expiration=${JWT_EXPIRATION:31536000000}
jwt.refresh-expiration=${JWT_REFRESH_EXPIRATION:604800000}

# ---- Mail ----
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${SMTP_USERNAME}
spring.mail.password=${SMTP_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

# ---- Google OAuth ----
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.redirect-uri=${GOOGLE_REDIRECT_URI}
spring.security.oauth2.client.registration.google.scope=profile,email
spring.security.oauth2.client.registration.google.client-name=Google

# ---- LangChain/Gemini ----
langchain4j.google-ai-gemini.chat-model.model-name=${GEMINI_MODEL_NAME:gemini-2.0-flash-lite}
langchain4j.google-ai-gemini.chat-model.api-key=${GEMINI_API_KEY}
langchain4j.google-ai-gemini.chat-model.log-requests-and-responses=true
langchain4j.google-ai-gemini.chat-model.max-output-tokens=500
28 changes: 26 additions & 2 deletions src/test/java/grep/neogulcoder/domain/IntegrationTestSupport.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
package grep.neogulcoder.domain;

import grep.neogulcoder.domain.review.service.ReviewService;
import grep.neogulcoder.domain.review.repository.MyReviewTagRepository;
import grep.neogulcoder.domain.review.repository.ReviewRepository;
import grep.neogulcoder.domain.review.repository.ReviewTagRepository;
import grep.neogulcoder.domain.review.service.ReviewService;
import grep.neogulcoder.domain.study.repository.StudyMemberRepository;
import grep.neogulcoder.domain.study.repository.StudyRepository;
import grep.neogulcoder.domain.users.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import grep.neogulcoder.global.auth.jwt.JwtAuthenticationFilter;
import grep.neogulcoder.global.auth.jwt.JwtExceptionFilter;
import grep.neogulcoder.global.auth.repository.UserBlackListRepository;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;

@Transactional
@ActiveProfiles("test")
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Import(IntegrationTestSupport.SecurityMocks.class)
public abstract class IntegrationTestSupport {

@TestConfiguration
static class SecurityMocks {
@Bean JwtAuthenticationFilter jwtAuthenticationFilter() {
return Mockito.mock(JwtAuthenticationFilter.class);
}
@Bean JwtExceptionFilter jwtExceptionFilter() {
return Mockito.mock(JwtExceptionFilter.class);
}
@Bean UserBlackListRepository userBlackListRepository() {
return Mockito.mock(UserBlackListRepository.class);
}
}

@Autowired
private UserRepository userRepository;

Expand Down
Loading