Skip to content

Commit 9e8d9cf

Browse files
committed
feat: 유저 충전 api 개발 및 E2E 테스트 개발
1 parent 8c0ba85 commit 9e8d9cf

9 files changed

Lines changed: 108 additions & 12 deletions

File tree

apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.loopers.domain.user.User;
44
import com.loopers.domain.user.UserRepository;
5+
import com.loopers.interfaces.api.point.PointV1Dto.PointResponse;
56
import com.loopers.support.error.CoreException;
67
import com.loopers.support.error.ErrorType;
78
import lombok.RequiredArgsConstructor;
@@ -19,10 +20,11 @@ public Integer getPoint(String userId) {
1920
.orElse(null);
2021
}
2122

22-
public void charge(String userId, int amount) {
23+
public PointResponse charge(String userId, int amount) {
2324
User user = userRepository.findByUserId(userId)
2425
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다."));
26+
int chargePoint = user.chargePoint(amount);
2527

26-
user.chargePoint(amount);
28+
return PointResponse.from(chargePoint);
2729
}
2830
}

apps/commerce-api/src/main/java/com/loopers/domain/user/User.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ public int getPoint() {
9393
return point;
9494
}
9595

96-
public void chargePoint(int amount) {
96+
public int chargePoint(int amount) {
9797
if (amount <= 0) {
9898
throw new CoreException(ErrorType.BAD_REQUEST, "충전 금액은 0보다 커야 합니다.");
9999
}
100100

101-
this.point += amount;
101+
return this.point += amount;
102102
}
103103
}

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ public interface PointV1ApiSpec {
1010
summary = "포인트 조회",
1111
description = "헤더의 ID로 보유 포인트를 조회합니다."
1212
)
13-
ApiResponse<PointV1Dto.PointResponse> getPoints(
13+
ApiResponse<PointV1Dto.PointResponse> getPoint(
1414
@Schema(name = "X-USER-ID", description = "조회할 사용자 ID (헤더)")
1515
String userId
1616
);
17+
18+
@Operation(
19+
summary = "포인트 충전",
20+
description = "ID에 해당하는 사용자의 포인트를 충전합니다."
21+
)
22+
ApiResponse<PointV1Dto.PointResponse> chargePoint(
23+
PointV1Dto.ChargePointsRequest request
24+
);
1725
}

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.loopers.domain.point.PointService;
44
import com.loopers.interfaces.api.ApiResponse;
5+
import com.loopers.interfaces.api.point.PointV1Dto.ChargePointsRequest;
56
import com.loopers.interfaces.api.point.PointV1Dto.PointResponse;
67
import com.loopers.support.error.CoreException;
78
import com.loopers.support.error.ErrorType;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
1013
import org.springframework.web.bind.annotation.RequestHeader;
1114
import org.springframework.web.bind.annotation.RequestMapping;
1215
import org.springframework.web.bind.annotation.RestController;
@@ -20,12 +23,21 @@ public class PointV1Controller implements PointV1ApiSpec {
2023

2124
@GetMapping()
2225
@Override
23-
public ApiResponse<PointResponse> getPoints(@RequestHeader(value = "X-USER-ID", required = false) String userId) {
26+
public ApiResponse<PointResponse> getPoint(@RequestHeader(value = "X-USER-ID", required = false) String userId) {
2427
if (userId == null || userId.isBlank()) {
2528
throw new CoreException(ErrorType.BAD_REQUEST, "필수 헤더인 X-USER-ID가 없거나 유효하지 않습니다.");
2629
}
2730
Integer point = pointService.getPoint(userId);
2831
PointResponse response = new PointResponse(point);
2932
return ApiResponse.success(response);
3033
}
34+
35+
@PostMapping()
36+
@Override
37+
public ApiResponse<PointResponse> chargePoint(@RequestBody ChargePointsRequest request) {
38+
PointResponse response = pointService.charge(request.userId(), request.point());
39+
return ApiResponse.success(response);
40+
}
41+
42+
3143
}
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
package com.loopers.interfaces.api.point;
22

3+
import jakarta.validation.constraints.NotNull;
4+
35
public class PointV1Dto {
46

5-
public record PointResponse(Integer point) {}
7+
public record PointResponse(Integer point) {
8+
9+
public static PointResponse from(int chargePoint) {
10+
return new PointResponse(
11+
chargePoint
12+
);
13+
}
14+
}
15+
16+
public record ChargePointsRequest(
17+
@NotNull String userId,
18+
@NotNull int point
19+
) {}
620
}

apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ public interface UserV1ApiSpec {
1717

1818
@Operation(
1919
summary = "내 정보 조회",
20-
description = "헤더의 ID로 내 정보를 조회합니다."
20+
description = "유저의 ID로 내 정보를 조회합니다."
2121
)
2222
ApiResponse<UserV1Dto.UserResponse> getMyInfo(
23-
@Schema(name = "USER-ID", description = "조회할 사용자 ID (헤더)")
23+
@Schema(name = "userId-ID", description = "조회할 사용자 ID")
2424
String userId
2525
);
2626

apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public ApiResponse<UserResponse> getMyInfo(@PathVariable(value = "userId") Strin
3535
User user = userService.getUser(userId);
3636

3737
if(user == null) {
38-
throw new CoreException(ErrorType.NOT_FOUND, "[userId = " + userId + "] 예시를 찾을 수 없습니다.");
38+
throw new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다.");
3939
}
4040
UserResponse response = UserResponse.from(user);
4141
return ApiResponse.success(response);

apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,10 @@ void chargePoint_with_positive_amount() {
164164
int expectedPoint = 100;
165165

166166
// act
167-
user.chargePoint(chargeAmount);
167+
int chargePoint = user.chargePoint(chargeAmount);
168168

169169
// assert
170-
assertEquals(expectedPoint, user.getPoint());
170+
assertEquals(expectedPoint, chargePoint);
171171
}
172172

173173
private void assertChargePointFails(int invalidAmount) {

apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.loopers.interfaces.api;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
34
import static org.junit.jupiter.api.Assertions.assertAll;
45
import static org.junit.jupiter.api.Assertions.assertEquals;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -8,6 +9,7 @@
89
import com.loopers.domain.user.User.Gender;
910
import com.loopers.domain.user.UserRepository;
1011
import com.loopers.interfaces.api.point.PointV1Dto;
12+
import com.loopers.interfaces.api.point.PointV1Dto.ChargePointsRequest;
1113
import com.loopers.utils.DatabaseCleanUp;
1214
import org.junit.jupiter.api.AfterEach;
1315
import org.junit.jupiter.api.DisplayName;
@@ -27,6 +29,7 @@
2729
class PointV1ApiE2ETest {
2830

2931
private static final String ENDPOINT_GET = "/api/v1/point";
32+
private static final String ENDPOINT_POST = "/api/v1/point";
3033

3134
private final TestRestTemplate testRestTemplate;
3235
private final UserRepository userRepository;
@@ -79,6 +82,8 @@ void returnsPoint_whenHeaderIsProvided() {
7982
);
8083
}
8184

85+
86+
8287
@DisplayName("X-USER-ID 헤더가 없을 경우, 400 Bad Request 응답을 반환한다.")
8388
@Test
8489
void returnsBadRequest_whenHeaderIsMissing() {
@@ -95,7 +100,62 @@ void returnsBadRequest_whenHeaderIsMissing() {
95100
() -> assertTrue(response.getStatusCode().is4xxClientError()),
96101
() -> assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode())
97102
);
103+
}
104+
}
105+
106+
@DisplayName("Post /api/v1/point")
107+
@Nested
108+
class Post {
109+
110+
@DisplayName("존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.")
111+
@Test
112+
void returnsPoint_whenHeaderIsProvided() {
113+
// arrange
114+
int expectedPoint = 1000;
115+
116+
User user = userRepository.save(
117+
new User("validId10", "valid@email.com", "2025-10-28", Gender.FEMALE)
118+
);
119+
120+
ChargePointsRequest request = new ChargePointsRequest(user.getUserId(), expectedPoint);
121+
122+
// act
123+
ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
124+
125+
ResponseEntity<ApiResponse<PointV1Dto.PointResponse>> response =
126+
testRestTemplate.exchange(ENDPOINT_POST, HttpMethod.POST, new HttpEntity<>(request), responseType);
127+
128+
// assert
129+
assertAll(
130+
() -> assertTrue(response.getStatusCode().is2xxSuccessful()),
131+
() -> assertThat(response.getBody().data().point()).isEqualTo(expectedPoint)
132+
);
133+
}
134+
135+
136+
137+
@DisplayName("존재하지 않는 유저로 요청할 경우, 404 Not Found 응답을 반환한다.")
138+
@Test
139+
void returnsPoint_whenHeaderddIsProvided() {
140+
// arrange
141+
String invalidUserId = "non-existent-user-id";
142+
int expectedPoint = 1000;
143+
144+
ChargePointsRequest request = new ChargePointsRequest(invalidUserId, expectedPoint);
98145

146+
// act
147+
ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
148+
149+
ResponseEntity<ApiResponse<PointV1Dto.PointResponse>> response =
150+
testRestTemplate.exchange(ENDPOINT_POST, HttpMethod.POST, new HttpEntity<>(request), responseType);
151+
152+
// assert
153+
assertAll(
154+
() -> assertTrue(response.getStatusCode().is4xxClientError()),
155+
() -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
156+
);
99157
}
100158
}
159+
160+
101161
}

0 commit comments

Comments
 (0)