Skip to content
Open
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: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(17)
}
}

Expand All @@ -25,7 +25,7 @@ ext {
dependencies {
implementation 'com.github.Miche-Let:common:dev-SNAPSHOT'
implementation 'com.github.Miche-Let:common-auth-webmvc:0.1.5'
implementation 'com.github.Miche-Let:common-auth-feign:0.1.1'
implementation 'com.github.Miche-Let:common-auth-feign:0.1.2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand Down
139 changes: 139 additions & 0 deletions http-tests/LOCAL_TEST_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Local HTTP Client Test Guide

IntelliJ HTTP Client 기반 로컬 통합 테스트 가이드.
모든 서비스가 Eureka에 등록되고, FeignClient는 `lb://` 로드밸런싱으로 내부 서비스를 호출합니다.

단, 이는 예약 생성 시나리오, 타 시나리오상 예약 서비스가 활용되는 내부 api 만한정하여 작성함.

---

## 환경 설정

### 서비스 설정 및 Java 코드 변경

# waiting-service

application.yml
- Eureka 클라이언트 설정 추가 (서비스 등록 및 검색)
- port 19200
build.gradle
- Spring Cloud Netflix Eureka Client 의존성 추가

# user-service

application.yml 설정

eureka:
client:
service-url:
defaultZone: http://localhost:${EUREKA_PORT:8761}/eureka/

spring:
datasource:
url: jdbc:postgresql://localhost:${POSTGRES_PORT:5432}/${POSTGRES_DB:michelet_db}
username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:admin}
driver-class-name: org.postgresql.Driver
data:
redis:
host: localhost
port: ${REDIS_PORT:6379}

jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
default_schema: user_service
format_sql: true
show-sql: true

# timeslot-service

application.yml 설정

eureka:
client:
service-url:
defaultZone: http://localhost:${EUREKA_PORT:8761}/eureka/

spring:
datasource:
url: jdbc:postgresql://localhost:${POSTGRES_PORT:5432}/${POSTGRES_DB:michelet_db}
username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:admin}
driver-class-name: org.postgresql.Driver
Comment on lines +60 to +65

jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
default_schema: timeslot_service
format_sql: true
show-sql: true

# restaurant-service

application.yml 설정

eureka:
client:
service-url:
defaultZone: http://localhost:${EUREKA_PORT:8761}/eureka/

spring:
datasource:
url: jdbc:postgresql://localhost:${POSTGRES_PORT:5432}/${POSTGRES_DB:michelet_db}
username: ${POSTGRES_USER:admin}
password: ${POSTGRES_PASSWORD:admin}
driver-class-name: org.postgresql.Driver
Comment on lines +84 to +90

jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
default_schema: restaurant_service
format_sql: true
show-sql: true

---

## 실행 순서

### 1. 인프라 및 DB 준비

```bash
# 1. 인프라 시작 (PostgreSQL, Redis 등)
cd infra && docker-compose -f docker-compose.infra.yml up -d

# 2. 테스트 데이터 리셋 및 시드 투입 (필수)
docker exec -i db psql -U admin -d michelet_db < http-tests/sql/reset_test_data.sql
docker exec -i db psql -U admin -d michelet_db < http-tests/sql/seed_test_data.sql
Comment on lines +109 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

명령 블록의 작업 디렉터리 전환 때문에 SQL 경로가 깨질 수 있습니다.

cd infra 이후 같은 세션에서 다음 줄을 실행하면 http-tests/sql/... 상대경로가 실패할 수 있습니다.

🔧 수정 제안
-# 1. 인프라 시작 (PostgreSQL, Redis 등)
-cd infra && docker-compose -f docker-compose.infra.yml up -d
+# 1. 인프라 시작 (PostgreSQL, Redis 등)
+docker compose -f infra/docker-compose.infra.yml up -d

 # 2. 테스트 데이터 리셋 및 시드 투입 (필수)
 docker exec -i db psql -U admin -d michelet_db < http-tests/sql/reset_test_data.sql
 docker exec -i db psql -U admin -d michelet_db < http-tests/sql/seed_test_data.sql
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http-tests/LOCAL_TEST_GUIDE.md` around lines 109 - 113, The current guide
changes directory with "cd infra && docker-compose -f docker-compose.infra.yml
up -d" which breaks the subsequent relative SQL paths used by the two "docker
exec -i db psql -U admin -d michelet_db < http-tests/sql/..." commands; fix by
avoiding a persistent cd (either run docker-compose from the repo root with
"docker-compose -f infra/docker-compose.infra.yml up -d" or scope the cd in a
subshell "(cd infra && docker-compose -f docker-compose.infra.yml up -d)"),
leaving the two docker exec lines' relative paths unchanged, or alternatively
convert those SQL paths to repository-root absolute/anchored paths (e.g.,
"./http-tests/sql/...") so they resolve correctly regardless of the current
working directory.

```
Comment on lines +105 to +114

### 2. 서비스 기동

`start-local.sh`를 사용하거나 별도 터미널에서 순서대로 기동합니다. (Eureka → User → Restaurant → Timeslot → Waiting → Reservation → Gateway)
실행후 Eureka 대시보드(http://localhost:8761)에서 모든 서비스가 `UP` 상태인지 확인합니다.

### 3. 테스트 실행 (IntelliJ HTTP Client)

#### **Scenario 1: 소비자 예약 플로우 (분리 실행)**

1. **`scenario1a_setup.http`**: 회원가입부터 대기열 등록(STEP 6)까지 한 번에 실행.
2. **10~20초 대기**: 스케줄러가 대기 토큰을 `ACTIVE`로 바꿀 때까지 기다립니다.
3. **`scenario1b_reservation.http`**:
- **STEP 7**을 먼저 실행하여 `ACTIVE` 상태를 확인합니다.
- 활성화가 확인되면 나머지 단계(예약 생성 등)를 진행합니다.

#### **Scenario 2: 체크인 플로우 (`scenario2_checkin.http`)**

- 시드 데이터의 고정 예약(`b2c3d4e5-...`)을 사용하여 체크인(`COMPLETED`)을 테스트합니다.
- `http-client.env.json`에 미리 설정된 ID를 사용하므로 바로 실행 가능합니다.

#### **Scenario 3: 방문 이력 검증 (`scenario3_history_check.http`)**

- 시드 데이터의 고정 예약(`a1b2c3d4-...`)을 사용합니다.
- 이 데이터는 항상 `CONFIRMED` 상태를 유지하므로, 시나리오 2 실행 여부와 관계없이 항상 성공합니다.
18 changes: 18 additions & 0 deletions http-tests/http-client.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"local": {
"gateway": "http://localhost:19000",
"reservationServiceUrl": "http://localhost:19500",
"accessToken": "",
"userId": "ffa8b6c9-cc64-41e8-a1b0-2b0622489b78",
"restaurantId": "2589a05e-db8f-4e62-bb3d-22ce85b750a3",
"timeSlotId": "",
"slotStartTime": "",
"courseId": "7d376f47-8fb3-4be1-916f-4ee0e6ee8b3b",
"courseUnitPrice": 80000,
"waitingId": "",
"waitingToken": "",
"reservationId": "b2c3d4e5-f6a7-8901-bcde-f01234567891",
"reservationIdForHistory": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"targetDate": "2026-06-14"
}
}
129 changes: 129 additions & 0 deletions http-tests/scenario1a_setup.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
### Scenario 1-A: Consumer Setup (SignUp -> Waiting)
### 사전 조건: 인프라 및 모든 서비스 기동 완료
### 이 파일은 대기열 등록(STEP 6)까지만 수행합니다. 이후 대기 상태가 ACTIVE가 되면 Scenario 1-B를 실행하세요.

### [STEP 1] 회원가입
POST {{gateway}}/api/v1/users/signup
Content-Type: application/json

{
"loginId": "testuser01",
"password": "Test123!@#",
"name": "테스트유저",
"email": "testuser01@example.com",
"phone": "010-1234-5678",
"userRole": "USER"
}

> {%
client.test("회원가입 성공", function() {
client.assert(response.status === 201, "Expected 201 Created but got " + response.status);
if (response.body && response.body.data) {
client.assert(response.body.data.userId !== undefined, "userId 반환 확인");
}
Comment on lines +21 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

핵심 필드 검증이 조건부라 초기 단계 오탐이 발생할 수 있습니다.

현재 구조는 본문 누락 시 실패하지 않아, 다음 STEP에서 이전 전역값을 재사용하며 시나리오가 왜곡될 수 있습니다. 각 STEP에서 필수 필드 존재를 조건문 밖에서 강제하는 편이 안전합니다.

수정 패턴 예시
-  if (response.body && response.body.data) {
-      client.assert(response.body.data.userId !== undefined, "userId 반환 확인");
-  }
+  client.assert(response.body && response.body.data, "response.body.data 누락");
+  client.assert(response.body.data.userId !== undefined, "userId 반환 확인");

Also applies to: 44-46, 62-64, 94-96, 120-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http-tests/scenario1a_setup.http` around lines 21 - 23, The check for
required fields is currently guarded by an if and thus silently skips failing
when response.body or response.body.data is missing; change the logic to assert
existence unconditionally by using client.assert to verify response.body exists,
response.body.data exists, and response.body.data.userId is not undefined
(replace the conditional block around response.body.data.userId with explicit
assertions using client.assert on response.body, response.body.data, and
response.body.data.userId); apply the same pattern for the other occurrences
that validate required fields (the blocks around lines referencing
response.body.data at the noted occurrences).

});
if (response.body && response.body.data) {
client.global.set("userId", response.body.data.userId);
}
%}

###

### [STEP 2] 로그인 → accessToken 추출
POST {{gateway}}/api/v1/auth/login
Content-Type: application/json

{
"loginId": "testuser01",
"password": "Test123!@#"
}

> {%
client.test("로그인 성공", function() {
client.assert(response.status === 200, "Expected 200 OK but got " + response.status);
if (response.body && response.body.data) {
client.assert(response.body.data.accessToken !== undefined, "accessToken 반환 확인");
}
});
if (response.body && response.body.data) {
client.global.set("accessToken", response.body.data.accessToken);
}
%}

###

### [STEP 3] 레스토랑 목록 검색
GET {{gateway}}/api/v1/restaurants?page=0&size=5
Authorization: Bearer {{accessToken}}

> {%
client.test("레스토랑 목록 조회 성공", function() {
client.assert(response.status === 200, "Expected 200 OK but got " + response.status);
if (response.body && response.body.data && response.body.data.content) {
client.assert(response.body.data.content.length > 0, "레스토랑 1개 이상 존재 확인");
}
});
if (response.body && response.body.data && response.body.data.content && response.body.data.content.length > 0) {
var first = response.body.data.content[0];
client.global.set("restaurantId", first.restaurantId);
}
%}

###

### [STEP 4] 레스토랑 상세 조회 (코스 목록 포함)
GET {{gateway}}/api/v1/restaurants/{{restaurantId}}
Authorization: Bearer {{accessToken}}

> {%
client.test("레스토랑 상세 조회 성공", function() {
client.assert(response.status === 200, "Expected 200 OK");
client.assert(response.body.data.restaurantId !== undefined, "restaurantId 확인");
});
%}

###

### [STEP 5] 코스 목록 조회 → courseId, 단가 추출
GET {{gateway}}/api/v1/restaurants/{{restaurantId}}/courses
Authorization: Bearer {{accessToken}}

> {%
client.test("코스 목록 조회 성공", function() {
client.assert(response.status === 200, "Expected 200 OK but got " + response.status);
if (response.body && response.body.data) {
client.assert(response.body.data.length > 0, "코스 1개 이상 존재 확인");
}
});
if (response.body && response.body.data && response.body.data.length > 0) {
var course = response.body.data[0];
client.global.set("courseId", course.courseId);
client.global.set("courseUnitPrice", course.price);
}
%}

###

### [STEP 6] 대기열 등록 → waitingId, waitingToken 추출
POST {{gateway}}/api/v1/waitings
Authorization: Bearer {{accessToken}}
Content-Type: application/json

{
"userId": "{{userId}}",
"restaurantId": "{{restaurantId}}"
}

> {%
client.test("대기열 등록 성공", function() {
client.assert(response.status === 201, "Expected 201 Created but got " + response.status);
if (response.body && response.body.data) {
client.assert(response.body.data.waitingId !== undefined, "waitingId 반환 확인");
client.assert(response.body.data.token !== undefined, "token 반환 확인");
}
});
if (response.body && response.body.data) {
client.global.set("waitingId", response.body.data.waitingId);
client.global.set("waitingToken", response.body.data.token);
}
%}
Loading
Loading