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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
pull_request:
branches: [main, develop]

concurrency:
group: ci-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

# 통합 테스트는 Testcontainers로 Postgres를 띄운다. GitHub 호스티드 러너에는 Docker가 기본 설치돼 있다.
- name: Test
run: ./gradlew test --console=plain

- name: Upload test report
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-report
path: build/reports/tests/test
retention-days: 7
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
package com.bcsdlab.bcsdinternalapiv2;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@TestPropertySource(properties = "app.jwt.secret=test-only-secret-key-not-for-production-32bytes-min")
class BcsdInternalApiV2ApplicationTests {

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
class BcsdInternalApiV2ApplicationTests extends IntegrationTestSupport {

@Test
void contextLoads() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.bcsdlab.bcsdinternalapiv2;

import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
Expand All @@ -13,19 +17,28 @@
* 통합 테스트 공통 베이스. Postgres 컨테이너를 테스트 클래스마다 새로 띄우지 않도록,
* 정적 초기화 블록에서 JVM당 한 번만 기동하는 Testcontainers 공식 "싱글턴 컨테이너" 패턴을 쓴다.
*
* <p>{@code @Testcontainers}/{@code @Container}를 쓰지 않는 이유: 그 조합은 컨테이너 생명주기를
* 테스트 클래스 단위로 관리해서(클래스마다 기동), 클래스가 늘어날수록 기동 비용이 누적된다.
* 여기서는 정적 필드를 상속받는 모든 하위 클래스가 같은 컨테이너 인스턴스를 공유한다.
* <p>{@code @Testcontainers}/{@code @Container}를 쓰지 않는 이유: 그 조합은 컨테이너를 테스트 클래스가
* 끝날 때 내리는데, Spring은 같은 설정의 컨텍스트를 다음 클래스에서 재사용하므로 두 번째 클래스부터
* 이미 내려간 컨테이너에 연결하려다 실패한다.
*
* <p>기존 7개 테스트 클래스(auth 4, member 2, 컨텍스트 로딩 1)는 각자 컨테이너를 띄우는 방식을
* 그대로 쓴다 — 이 클래스로의 이전은 선택적 후속 작업이다(T-01).
* <p>모든 통합 테스트가 같은 DB를 쓰므로, 테스트마다 마이그레이션이 넣는 기준 데이터를 뺀 전 테이블을
* 비운다. 각 테스트의 {@code @BeforeEach}는 이 메서드 다음에 실행된다.
*/
@SpringBootTest
@AutoConfigureMockMvc
public abstract class IntegrationTestSupport {

/** Flyway 이력과 마이그레이션이 시드하는 기준 데이터 테이블. */
private static final Set<String> PRESERVED_TABLES =
Set.of("flyway_schema_history", "track", "position", "dues_exemption_reason");

/**
* 설정(목 빈, 프로퍼티)이 다른 테스트 클래스마다 Spring 컨텍스트가 따로 캐시되고 각자 커넥션 풀을 잡는다.
* 모든 컨텍스트가 이 컨테이너 하나를 쓰므로 기본 연결 상한(100)을 넘지 않게 늘린다.
*/
@ServiceConnection
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withCommand("postgres", "-c", "max_connections=500");

static {
POSTGRES.start();
Expand All @@ -38,4 +51,15 @@ static void jwtSecret(DynamicPropertyRegistry registry) {

@Autowired
protected MockMvc mockMvc;

@Autowired
private JdbcTemplate jdbcTemplate;

@BeforeEach
void resetDatabase() {
List<String> tables = jdbcTemplate.queryForList(
"select tablename from pg_tables where schemaname = 'public'", String.class);
String targets = String.join(", ", tables.stream().filter(t -> !PRESERVED_TABLES.contains(t)).toList());
jdbcTemplate.execute("TRUNCATE " + targets + " CASCADE");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.auth.model.RefreshToken;
import com.bcsdlab.bcsdinternalapiv2.auth.repository.RefreshTokenRepository;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
Expand All @@ -24,36 +25,12 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class InitialSetupIntegrationTest {

private static final String RAW_PASSWORD = "Temp1234";

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

@DynamicPropertySource
static void jwtSecret(DynamicPropertyRegistry registry) {
registry.add("app.jwt.secret", () -> "test-only-secret-key-not-for-production-32bytes-min");
}
class InitialSetupIntegrationTest extends IntegrationTestSupport {

@Autowired
private MockMvc mockMvc;
private static final String RAW_PASSWORD = "Temp1234";

private TrackMaster backend;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.auth.repository.RefreshTokenRepository;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
import com.bcsdlab.bcsdinternalapiv2.member.model.MemberStatus;
Expand All @@ -22,36 +23,12 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class LoginIntegrationTest {

private static final String RAW_PASSWORD = "Temp1234";

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

@DynamicPropertySource
static void jwtSecret(DynamicPropertyRegistry registry) {
registry.add("app.jwt.secret", () -> "test-only-secret-key-not-for-production-32bytes-min");
}
class LoginIntegrationTest extends IntegrationTestSupport {

@Autowired
private MockMvc mockMvc;
private static final String RAW_PASSWORD = "Temp1234";

private TrackMaster backend;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.auth.exception.AuthException;
import com.bcsdlab.bcsdinternalapiv2.auth.model.PasswordResetToken;
import com.bcsdlab.bcsdinternalapiv2.auth.model.RefreshToken;
Expand All @@ -30,38 +31,14 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class PasswordResetIntegrationTest {

private static final String RAW_PASSWORD = "Temp1234";

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

@DynamicPropertySource
static void jwtSecret(DynamicPropertyRegistry registry) {
registry.add("app.jwt.secret", () -> "test-only-secret-key-not-for-production-32bytes-min");
}
class PasswordResetIntegrationTest extends IntegrationTestSupport {

@Autowired
private MockMvc mockMvc;
private static final String RAW_PASSWORD = "Temp1234";

private TrackMaster backend;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.auth.model.RefreshToken;
import com.bcsdlab.bcsdinternalapiv2.auth.repository.RefreshTokenRepository;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
Expand All @@ -23,36 +24,12 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class RefreshTokenIntegrationTest {

private static final String RAW_PASSWORD = "Temp1234";

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

@DynamicPropertySource
static void jwtSecret(DynamicPropertyRegistry registry) {
registry.add("app.jwt.secret", () -> "test-only-secret-key-not-for-production-32bytes-min");
}
class RefreshTokenIntegrationTest extends IntegrationTestSupport {

@Autowired
private MockMvc mockMvc;
private static final String RAW_PASSWORD = "Temp1234";

private TrackMaster backend;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.member.client.SlackClient;
import com.bcsdlab.bcsdinternalapiv2.member.model.AcademicStatus;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
Expand All @@ -18,38 +19,14 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class AdminMemberIntegrationTest {
class AdminMemberIntegrationTest extends IntegrationTestSupport {

private static final String RAW_PASSWORD = "Temp1234";

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

@DynamicPropertySource
static void jwtSecret(DynamicPropertyRegistry registry) {
registry.add("app.jwt.secret", () -> "test-only-secret-key-not-for-production-32bytes-min");
}

@Autowired
private MockMvc mockMvc;

// 부원 추가가 이메일로 Slack ID를 조회한다 — 테스트에서 실제 slack.com을 부르지 않게 막는다.
@MockitoBean
private SlackClient slackClient;
Expand Down
Loading
Loading