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
14 changes: 7 additions & 7 deletions src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,31 @@ include::{snippets}/auth-controller-test/kakao-logout/response-body.adoc[]

== 토큰 상태 확인

쿠키에 담긴 토큰의 유효성을 확인합니다
쿠키에 담긴 토큰의 유효성을 확인합니다.

=== Example

include::{snippets}/auth-controller-test/check-auth_-success/http-request.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]

=== HTTP

==== 요청

include::{snippets}/auth-controller-test/check-auth_-success/curl-request.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]
Comment on lines +110 to +116
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

=== Example==== 요청 섹션의 스니펫 파일이 다른 섹션과 반대로 포함되어 있습니다

다른 API 섹션(게스트 토큰 발급, 액세스 토큰 재발급 등)은 모두 아래 규칙을 따릅니다:

  • === Examplecurl-request.adoc
  • ==== 요청http-request.adoc

그런데 토큰 상태 확인 섹션(110, 116번 라인)은 반대로 포함되어 있습니다.

📝 수정 제안
 === Example

-include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]
+include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]

 === HTTP

 ==== 요청

-include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]
+include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]
=== HTTP
==== 요청
include::{snippets}/auth-controller-test/check-auth_-success/curl-request.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]
=== HTTP
==== 요청
include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/docs/asciidoc/auth.adoc` around lines 110 - 116, 현재 토큰 상태 확인 섹션에서 '===
Example'과 '==== 요청'의 스니펫이 역전되어 포함되어 있으니, auth.adoc의 해당 블록에서
include::{snippets}/auth-controller-test/check-auth-success/http-request.adoc[]를
'==== 요청' 아래로 옮기고
include::{snippets}/auth-controller-test/check-auth-success/curl-request.adoc[]를
'=== Example' 아래로 교체하여 다른 API 섹션과 동일한 규칙(=== Example → curl-request.adoc, ====
요청 → http-request.adoc)을 따르도록 수정하세요.


==== 응답

include::{snippets}/auth-controller-test/check-auth_-success/http-response.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/http-response.adoc[]

=== Body

==== 응답 (valid)

include::{snippets}/auth-controller-test/check-auth_-success/response-body.adoc[]
include::{snippets}/auth-controller-test/check-auth-success/response-body.adoc[]

==== 응답 (invalid)

include::{snippets}/auth-controller-test/check-auth_-expired/response-body.adoc[]
include::{snippets}/auth-controller-test/check-auth-expired/response-body.adoc[]

include::{snippets}/auth-controller-test/check-auth_-invalid-token/response-body.adoc[]
include::{snippets}/auth-controller-test/check-auth-invalid-token/response-body.adoc[]

8 changes: 4 additions & 4 deletions src/docs/asciidoc/settlement.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,20 @@ user가 속한 정산의 공유 링크 리스트를 조회할 수 있다.

=== Example

include::{snippets}/settlement-controller-test/get-share-link-list_-success/curl-request.adoc[]
include::{snippets}/settlement-controller-test/get-share-link-list-success/curl-request.adoc[]

=== HTTP

==== 요청

include::{snippets}/settlement-controller-test/get-share-link-list_-success/http-request.adoc[]
include::{snippets}/settlement-controller-test/get-share-link-list-success/http-request.adoc[]

==== 응답

include::{snippets}/settlement-controller-test/get-share-link-list_-success/http-response.adoc[]
include::{snippets}/settlement-controller-test/get-share-link-list-success/http-response.adoc[]

=== Body

==== 응답

include::{snippets}/settlement-controller-test/get-share-link-list_-success/response-body.adoc[]
include::{snippets}/settlement-controller-test/get-share-link-list-success/response-body.adoc[]
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dnd.moddo.auth.presentation;

import static com.dnd.moddo.auth.infrastructure.security.JwtConstants.*;

import java.io.IOException;
import java.util.Collections;

Expand Down Expand Up @@ -100,8 +102,8 @@ public ResponseEntity<AuthCheckResponse> checkAuth(
try {
Claims claims = jwtProvider.parseClaims(token);

Long userId = jwtProvider.getUserId(token);
String role = jwtProvider.getRole(token);
Long userId = claims.get(AUTH_ID.getMessage(), Long.class);
String role = claims.get(ROLE.getMessage(), String.class);
Comment on lines +105 to +106
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

🧩 Analysis chain

🏁 Script executed:

# Find build configuration and JJWT version
find . -type f \( -name "*.gradle" -o -name "*.gradle.kts" -o -name "pom.xml" \) | head -20

Repository: moddo-kr/moddo-backend

Length of output: 98


🏁 Script executed:

# Search for JwtProvider class
rg -l "class JwtProvider" --type java

Repository: moddo-kr/moddo-backend

Length of output: 139


🏁 Script executed:

# Search for AuthCheckResponse class
rg -l "class AuthCheckResponse" --type java

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Find the target file to confirm lines 105-106
rg -n "Long userId = claims.get" --type java -A 2 -B 2

Repository: moddo-kr/moddo-backend

Length of output: 593


🏁 Script executed:

# Read build.gradle to find JJWT version
cat build.gradle | head -100

Repository: moddo-kr/moddo-backend

Length of output: 3050


🏁 Script executed:

# Find and read JwtProvider class
cat -n src/main/java/com/dnd/moddo/auth/infrastructure/security/JwtProvider.java

Repository: moddo-kr/moddo-backend

Length of output: 4937


🏁 Script executed:

# Search for AuthCheckResponse
rg -n "class AuthCheckResponse" --type java -A 30

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Search for UserInfo class
rg -n "class UserInfo" --type java -A 20

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Check what happens after lines 105-106 in AuthController
sed -n '105,130p' src/main/java/com/dnd/moddo/auth/presentation/AuthController.java

Repository: moddo-kr/moddo-backend

Length of output: 875


🏁 Script executed:

# Search more broadly for AuthCheckResponse and UserInfo
rg "AuthCheckResponse\|UserInfo" --type java -l

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Look at the full AuthController file around lines 105
sed -n '80,130p' src/main/java/com/dnd/moddo/auth/presentation/AuthController.java

Repository: moddo-kr/moddo-backend

Length of output: 1623


🏁 Script executed:

# Search for response classes
fd -e java -path "*/response/*" | xargs grep -l "AuthCheckResponse\|UserInfo" 2>/dev/null

Repository: moddo-kr/moddo-backend

Length of output: 236


🏁 Script executed:

# Find response classes in the presentation/response directory
find src/main/java/com/dnd/moddo -path "*/response/*" -name "*.java" -type f

Repository: moddo-kr/moddo-backend

Length of output: 2406


🏁 Script executed:

# Search for AuthCheckResponse definition
rg -n "class AuthCheckResponse" --type java -A 50

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Search for UserInfo definition
rg -n "class UserInfo" --type java -A 30

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Read the AuthCheckResponse file directly
cat -n src/main/java/com/dnd/moddo/auth/presentation/response/AuthCheckResponse.java

Repository: moddo-kr/moddo-backend

Length of output: 935


🏁 Script executed:

# Search for UserInfo more broadly
rg "class UserInfo" --type java -B 2 -A 20

Repository: moddo-kr/moddo-backend

Length of output: 48


🏁 Script executed:

# Search for `@JsonInclude` annotations
rg "@JsonInclude" --type java -B 2 -A 5

Repository: moddo-kr/moddo-backend

Length of output: 886


🏁 Script executed:

# Check if there are any null checks or validation in the auth check endpoint
sed -n '90,140p' src/main/java/com/dnd/moddo/auth/presentation/AuthController.java

Repository: moddo-kr/moddo-backend

Length of output: 1496


🏁 Script executed:

# Verify JJWT 0.11.5 behavior with null claims
web_search
JJWT 0.11.5 Claims.get() null handling Integer Long type coercion

Repository: moddo-kr/moddo-backend

Length of output: 267


🌐 Web query:

JJWT 0.11.5 Claims.get() null handling when claim is missing

💡 Result:

In JJWT 0.11.5, Claims is a Map<String,Object>, so claims.get("foo") returns null when the claim is missing (and it will also return null if the claim exists but its value is null, so you can’t distinguish those two cases with get alone). This is consistent with the library’s API contract that claim access returns null “if not present”. [1]

How to handle it:

  • If you need to distinguish “missing” vs “present but null”:

    • Use claims.containsKey("foo") (standard Map), and then claims.get("foo"). (Claims extends Map.) [1]
    • If you’re using the Claims-style API that exposes it, you can also use contains(claimName) which returns false when the claim does not exist. [2]
  • If you want a typed read:
    Use claims.get("foo", MyType.class), which also returns null if not present (and throws if present but of the wrong type). [1]

  • If you want “missing claim” to be an error instead of null:
    Use the parser “require…” methods so parsing fails with MissingClaimException / IncorrectClaimException when expectations aren’t met. [3]

Also note: when building tokens in 0.11.5, setting a claim to null removes it (so “missing” is the normal representation, not “present with null”). [4]

Sources
[1] JJWT Claims Javadoc (0.11.x): typed get(claimName, requiredType) returns null if not present; Claims extends Map https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.11.2/io/jsonwebtoken/Claims.html
[2] “Returns null if the claim not exist…” / contains(claimName) contract (Claims interface description) https://micronaut-projects.github.io/micronaut-security/2.0.0.RC1/api/io/micronaut/security/oauth2/endpoint/token/response/JWTOpenIdClaims.html
[3] JJWT GitHub docs: using require* methods and missing/incorrect claim exceptions https://github.com/jwtk/jjwt
[4] JwtBuilder.claim(name, value): “A null value will remove the property from the Claims.” https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.11.5/io/jsonwebtoken/JwtBuilder.html


🌐 Web query:

JJWT Integer to Long type coercion issue JSON parsing

💡 Result:

This happens because JWT claims are JSON, and when JJWT uses Jackson to deserialize the claims into an untyped Map, Jackson uses “natural” number mapping: small integral values become Integer, and only larger ones become Long. Then claims.get("x", Long.class) can fail (often as a RequiredTypeException / ClassCastException) because the stored value is actually an Integer, not a Long. [3], [2]

Fix options

1) Read as Number and convert (most robust)

Number n = claims.get("accountId", Number.class);
long accountId = n.longValue();

2) Configure JJWT’s Jackson ObjectMapper to force longs for ints
Enable Jackson’s USE_LONG_FOR_INTS, and plug that mapper into JJWT’s JacksonDeserializer:

ObjectMapper om = new ObjectMapper()
    .enable(DeserializationFeature.USE_LONG_FOR_INTS);

JwtParser parser = Jwts.parser()
    .json(new JacksonDeserializer(om))
    .build();

JJWT explicitly supports supplying your own ObjectMapper via JacksonDeserializer(ObjectMapper). [1], [2]

3) Store the claim as a string (if you control token creation and want to avoid numeric ambiguity)

claims.put("accountId", Long.toString(accountId));

Sources: JJWT JSON/Jackson customization docs [1], Jackson USE_LONG_FOR_INTS behavior [2], explanation of Integer vs Long claim deserialization causing typed-get failures [3].

References: [1] (github.com) [2] (fasterxml.github.io) [3] (stackoverflow.com)

Citations:


claims.get() 결과에 대한 null 처리 필수

JJWT 0.11.5에서 Claims.get(claimName, requiredType)은 클레임이 존재하지 않으면 null을 반환합니다. 현재 코드는 이 null 값에 대한 처리가 없어, userId나 role이 누락된 경우 다음과 같은 문제가 발생합니다:

  1. 응답에 null 필드 포함: UserInfo 레코드는 @JsonInclude 애너테이션이 없으므로, null 값이 그대로 JSON으로 직렬화됩니다. 결과적으로 {"authenticated": true, "user": {"id": null, "role": null}}이 클라이언트에 전송됩니다.
  2. Integer → Long 변환 문제: Jackson이 JSON의 작은 숫자를 Integer로 역직렬화하면, claims.get(AUTH_ID.getMessage(), Long.class)가 예외를 발생시키거나 null을 반환할 수 있습니다.

null 체크를 추가하세요:

수정 안내
 Claims claims = jwtProvider.parseClaims(token);

 Long userId = claims.get(AUTH_ID.getMessage(), Long.class);
 String role = claims.get(ROLE.getMessage(), String.class);

+if (userId == null || role == null) {
+    return ResponseEntity.ok(
+        AuthCheckResponse.fail(AuthCheckResponse.AuthFailReason.INVALID_TOKEN)
+    );
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Long userId = claims.get(AUTH_ID.getMessage(), Long.class);
String role = claims.get(ROLE.getMessage(), String.class);
Long userId = claims.get(AUTH_ID.getMessage(), Long.class);
String role = claims.get(ROLE.getMessage(), String.class);
if (userId == null || role == null) {
return ResponseEntity.ok(
AuthCheckResponse.fail(AuthCheckResponse.AuthFailReason.INVALID_TOKEN)
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/dnd/moddo/auth/presentation/AuthController.java` around
lines 105 - 106, In AuthController, guard the two claims.get(...) calls by
retrieving the id claim as a Number (or Object) first and converting it to Long
(e.g. Number idNum = claims.get(AUTH_ID.getMessage(), Number.class); Long userId
= idNum == null ? null : idNum.longValue()) and by checking the role claim for
null (String role = claims.get(ROLE.getMessage(), String.class)); if either is
null, return/throw an appropriate unauthorized/bad-token response instead of
allowing nulls into the UserInfo record (or provide a safe default), so replace
the existing direct claims.get(...) usage with null checks and Number→Long
conversion in the method that builds the UserInfo in AuthController.


return ResponseEntity.ok(
AuthCheckResponse.success(userId, role)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.dnd.moddo.auth.presentation.response;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public record AuthCheckResponse(
boolean authenticated,
UserInfo user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,9 @@ public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException e
) {

String message = e.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(error -> error.getField() + " : " + error.getDefaultMessage())
.orElse("잘못된 요청입니다.");
LoggingUtils.warn(e);

String message = e.getMessage();
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

e.getMessage()는 Spring 내부 구현 상세를 API 클라이언트에 그대로 노출합니다.

MethodArgumentNotValidException.getMessage()는 다음과 같이 Spring 내부의 장황한 문자열을 반환합니다:

Validation failed for argument [0] in public void ...com.example.Dto with 2 errors:
[Field error in object 'dto' on field 'name': rejected value [null];
codes [NotBlank.dto.name,...]; arguments [...]; default message [이름을 입력해주세요.]] ...

이 문자열에는 완전한 클래스명, 필드명, 제약 코드, 거부된 값 등이 포함되어 API 응답으로 반환되면 내부 모델 구조가 외부에 노출됩니다(정보 누출). 또한 이전 구현에서 사용자 친화적인 메시지(FieldError.getDefaultMessage())를 반환하던 것에서 회귀된 변경입니다.

모든 필드 오류 메시지를 수집하거나, 단일 필드 오류 메시지를 반환하는 방식으로 수정하는 것을 권장합니다.

🛡️ 수정 제안
 	`@ExceptionHandler`(MethodArgumentNotValidException.class)
 	public ResponseEntity<ErrorResponse> handleValidation(
 		MethodArgumentNotValidException e
 	) {
 		LoggingUtils.warn(e);
 
-		String message = e.getMessage();
+		String message = e.getBindingResult().getFieldErrors().stream()
+			.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
+			.collect(java.util.stream.Collectors.joining(", "));
 
 		return ResponseEntity.badRequest()
 			.body(new ErrorResponse(400, message));
 	}

여러 필드 오류를 모두 반환하기 어렵다면, 첫 번째 필드 오류만 추출하는 방식도 가능합니다:

-		String message = e.getMessage();
+		String message = e.getBindingResult().getFieldErrors().stream()
+			.findFirst()
+			.map(org.springframework.validation.FieldError::getDefaultMessage)
+			.orElse("잘못된 요청입니다.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String message = e.getMessage();
`@ExceptionHandler`(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException e
) {
LoggingUtils.warn(e);
String message = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(java.util.stream.Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, message));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/dnd/moddo/common/exception/GlobalExceptionHandler.java` at
line 75, The handler currently uses e.getMessage() in GlobalExceptionHandler for
MethodArgumentNotValidException which leaks internal Spring details; change the
handling in the catch for MethodArgumentNotValidException (the variable e) to
extract user-friendly messages from the BindingResult: collect
FieldError.getDefaultMessage() for all field errors (or at minimum use the first
FieldError.getDefaultMessage()), and return those messages in the API response
instead of e.getMessage(); locate the MethodArgumentNotValidException handling
branch in GlobalExceptionHandler and replace the direct e.getMessage() usage
with logic that calls e.getBindingResult().getFieldErrors() -> map to
FieldError::getDefaultMessage (or pick the first) and use that string/list in
the response.


return ResponseEntity.badRequest()
.body(new ErrorResponse(400, message));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.dnd.moddo.event.application.impl.SettlementValidator;
import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.settlement.Settlement;
import com.dnd.moddo.event.domain.settlement.type.SettlementSortType;
import com.dnd.moddo.event.domain.settlement.type.SettlementStatus;
import com.dnd.moddo.event.presentation.request.SearchSettlementListRequest;
import com.dnd.moddo.event.presentation.response.SettlementDetailResponse;
Expand Down Expand Up @@ -51,9 +52,12 @@ public List<SettlementListResponse> search(
SettlementStatus effectiveStatus =
request.status() == null ? SettlementStatus.ALL : request.status();

SettlementSortType effectiveSort =
request.sort() == null ? SettlementSortType.LATEST : request.sort();

int limit = request.limit() == null ? 10 : request.limit();

return settlementReader.findListByUserIdAndStatus(userId, effectiveStatus, request.sort(), limit);
return settlementReader.findListByUserIdAndStatus(userId, effectiveStatus, effectiveSort, limit);
}

public List<SettlementShareResponse> findSettlementShareList(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.springframework.stereotype.Repository;

import com.dnd.moddo.event.domain.expense.QExpense;
import com.dnd.moddo.event.domain.member.QMember;
import com.dnd.moddo.event.domain.settlement.QSettlement;
import com.dnd.moddo.event.domain.settlement.type.SettlementSortType;
Expand All @@ -15,6 +16,8 @@
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;
Expand All @@ -35,6 +38,7 @@ public List<SettlementListResponse> findByUserAndStatus(
) {
QSettlement settlement = QSettlement.settlement;
QMember member = QMember.member;
QExpense expense = QExpense.expense;

BooleanExpression userCondition =
member.user.id.eq(userId);
Expand All @@ -61,6 +65,12 @@ public List<SettlementListResponse> findByUserAndStatus(
member.isPaid
);

JPQLQuery<Long> totalAmount =
JPAExpressions
.select(expense.amount.sum().coalesce(0L))
.from(expense)
.where(expense.settlement.id.eq(settlement.id));

OrderSpecifier<?> orderSpecifier =
getOrderSpecifier(sortType, settlement, memberCount, completedCount);

Expand All @@ -70,6 +80,7 @@ public List<SettlementListResponse> findByUserAndStatus(
settlement.id,
settlement.code,
settlement.name,
totalAmount,
memberCount,
completedCount.coalesce(0L),
settlement.createdAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public record SearchSettlementListRequest(
@NotNull(message = "정렬 방식은 필수입니다.")
SettlementSortType sort,

@NotNull(message = "limit은 필수입니다.")
@Min(value = 1, message = "limit은 1 이상이어야 합니다.")
@Max(value = 100, message = "limit은 최대 100까지 가능합니다.")
Integer limit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public record SettlementListResponse(
Long groupId,
String groupCode,
String name,
Long totalAmount,
Long totalMemberCount,
Long completedMemberCount,
LocalDateTime createdAt,
Expand Down
63 changes: 63 additions & 0 deletions src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.moddo.domain.Member.entity;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -10,6 +11,7 @@
import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.settlement.Settlement;
import com.dnd.moddo.global.support.GroupTestFactory;
import com.dnd.moddo.user.domain.User;

class MemberTest {

Expand Down Expand Up @@ -74,4 +76,65 @@ void testUpdatePaymentStatus() {
assertThat(member.isPaid()).isTrue();
assertThat(member.getPaidAt()).isNotNull();
}

@DisplayName("사용자를 정상적으로 할당할 수 있다.")
@Test
void assignUser_success() {
// given
Member member = Member.builder()
.name("기존이름")
.settlement(mockSettlement)
.role(ExpenseRole.PARTICIPANT)
.isPaid(false)
.build();

User user = mock(User.class);
when(user.getName()).thenReturn("새이름");

// when
member.assignUser(user);

// then
assertThat(member.getUser()).isEqualTo(user);
assertThat(member.getName()).isEqualTo("새이름"); // 동기화 확인
}

@DisplayName("이미 사용자가 연결되어 있으면 예외가 발생한다.")
@Test
void assignUser_throwException_whenUserAlreadyAssigned() {
// given
Member member = Member.builder()
.name("기존이름")
.settlement(mockSettlement)
.role(ExpenseRole.PARTICIPANT)
.isPaid(false)
.build();

User firstUser = mock(User.class);
User secondUser = mock(User.class);

when(firstUser.getName()).thenReturn("첫번째");
when(secondUser.getName()).thenReturn("두번째");

member.assignUser(firstUser);

// when & then
assertThatThrownBy(() -> member.assignUser(secondUser))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("이미 사용자와 연결된 멤버입니다.");
Comment on lines +113 to +124
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find test configuration and MockitoExtension usage
rg -n --type=java "MockitoExtension|STRICT_STUBS|MockitoSettings" -C2

Repository: moddo-kr/moddo-backend

Length of output: 41559


🏁 Script executed:

#!/bin/bash
# Read the test file to verify the code snippet
find . -name "MemberTest.java" -type f | head -5

Repository: moddo-kr/moddo-backend

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Check for test setup configuration files
find . -name "*.properties" -o -name "*.yml" -o -name "*.yaml" | grep -E "(test|config)" | head -20

Repository: moddo-kr/moddo-backend

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Read the MemberTest.java file to verify the code
cat -n ./src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java | sed -n '100,135p'

Repository: moddo-kr/moddo-backend

Length of output: 1218


🏁 Script executed:

#!/bin/bash
# Find the Member entity class
find . -name "Member.java" -type f | grep -E "entity|domain" | grep -v Test

Repository: moddo-kr/moddo-backend

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Check test configuration for Mockito strict stubbing
cat ./src/test/resources/application.yml | head -30

Repository: moddo-kr/moddo-backend

Length of output: 686


🏁 Script executed:

#!/bin/bash
# Read the Member.java file to see assignUser method
cat -n ./src/main/java/com/dnd/moddo/event/domain/member/Member.java | grep -A 20 "assignUser"

Repository: moddo-kr/moddo-backend

Length of output: 695


🏁 Script executed:

#!/bin/bash
# Read MemberTest.java class header to check MockitoExtension
cat -n ./src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java | head -30

Repository: moddo-kr/moddo-backend

Length of output: 1079


🏁 Script executed:

#!/bin/bash
# Search for MockitoSettings or Strictness configuration in the test class
rg -n "MockitoSettings|Strictness" ./src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java

Repository: moddo-kr/moddo-backend

Length of output: 48


불필요한 스텁을 제거하세요.

Line 117의 when(secondUser.getName()).thenReturn("두번째")는 실제로 사용되지 않습니다. assignUser() 메서드는 사용자가 이미 연결되어 있는지 먼저 확인한 후 예외를 던지므로, secondUsergetName()이 호출되기 전에 IllegalStateException이 발생합니다. 해당 스텁은 dead code입니다.

제거 제안
  User firstUser = mock(User.class);
  User secondUser = mock(User.class);
  
  when(firstUser.getName()).thenReturn("첫번째");
- when(secondUser.getName()).thenReturn("두번째");
  
  member.assignUser(firstUser);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
User firstUser = mock(User.class);
User secondUser = mock(User.class);
when(firstUser.getName()).thenReturn("첫번째");
when(secondUser.getName()).thenReturn("두번째");
member.assignUser(firstUser);
// when & then
assertThatThrownBy(() -> member.assignUser(secondUser))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("이미 사용자와 연결된 멤버입니다.");
User firstUser = mock(User.class);
User secondUser = mock(User.class);
when(firstUser.getName()).thenReturn("첫번째");
member.assignUser(firstUser);
// when & then
assertThatThrownBy(() -> member.assignUser(secondUser))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("이미 사용자와 연결된 멤버입니다.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/dnd/moddo/domain/Member/entity/MemberTest.java` around
lines 113 - 124, The test contains an unused stub: remove the unnecessary
when(secondUser.getName()).thenReturn("두번째") in MemberTest since
assignUser(Member member) throws IllegalStateException before calling
secondUser.getName(); keep creating the secondUser mock but delete the getName()
stubbing so the test only verifies that member.assignUser(secondUser) throws the
expected exception.

}

@DisplayName("null 사용자를 할당하면 예외가 발생한다.")
@Test
void assignUser_throwException_whenUserIsNull() {
Member member = Member.builder()
.name("기존이름")
.settlement(mockSettlement)
.role(ExpenseRole.PARTICIPANT)
.isPaid(false)
.build();

assertThatThrownBy(() -> member.assignUser(null))
.isInstanceOf(NullPointerException.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dnd.moddo.domain.auth.controller;

import static com.dnd.moddo.auth.infrastructure.security.JwtConstants.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.cookies.CookieDocumentation.*;
import static org.springframework.restdocs.headers.HeaderDocumentation.*;
Expand Down Expand Up @@ -141,14 +142,14 @@ void kakaoLogout() throws Exception {

@Test
@DisplayName("토큰이 유효하면 인증 성공 응답을 반환한다.")
void checkAuth_Success() throws Exception {
void checkAuthSuccess() throws Exception {
// given
String token = "valid-token";

Claims claims = mock(Claims.class);
given(jwtProvider.parseClaims(token)).willReturn(claims);
given(jwtProvider.getUserId(token)).willReturn(1L);
given(jwtProvider.getRole(token)).willReturn("USER");
given(claims.get(AUTH_ID.getMessage(), Long.class)).willReturn(1L);
given(claims.get(ROLE.getMessage(), String.class)).willReturn("USER");

// when & then
mockMvc.perform(get("/api/v1/auth/check")
Expand All @@ -163,15 +164,14 @@ void checkAuth_Success() throws Exception {
fieldWithPath("authenticated").description("인증 여부"),
fieldWithPath("user").description("사용자 정보").optional(),
fieldWithPath("user.id").description("사용자 ID").optional(),
fieldWithPath("user.role").description("사용자 권한").optional(),
fieldWithPath("reason").description("실패 사유 (인증 실패 시)").optional()
fieldWithPath("user.role").description("사용자 권한").optional()
)
));
}

@Test
@DisplayName("토큰이 없으면 NO_TOKEN 응답을 반환한다.")
void checkAuth_NoToken() throws Exception {
void checkAuthNoToken() throws Exception {

mockMvc.perform(get("/api/v1/auth/check"))
.andExpect(status().isOk())
Expand All @@ -180,15 +180,14 @@ void checkAuth_NoToken() throws Exception {
.andDo(restDocs.document(
responseFields(
fieldWithPath("authenticated").description("인증 여부"),
fieldWithPath("user").description("사용자 정보").optional(),
fieldWithPath("reason").description("실패 사유 (NO_TOKEN)")
)
));
}

@Test
@DisplayName("토큰이 만료되면 TOKEN_EXPIRED 응답을 반환한다.")
void checkAuth_Expired() throws Exception {
void checkAuthExpired() throws Exception {
// given
String token = "expired-token";

Expand All @@ -203,15 +202,14 @@ void checkAuth_Expired() throws Exception {
.andDo(restDocs.document(
responseFields(
fieldWithPath("authenticated").description("인증 여부"),
fieldWithPath("user").description("사용자 정보").optional(),
fieldWithPath("reason").description("실패 사유 (TOKEN_EXPIRED)")
)
));
}

@Test
@DisplayName("토큰이 유효하지 않으면 INVALID_TOKEN 응답을 반환한다.")
void checkAuth_InvalidToken() throws Exception {
void checkAuthInvalidToken() throws Exception {
// given
String token = "invalid-token";

Expand All @@ -226,7 +224,6 @@ void checkAuth_InvalidToken() throws Exception {
.andDo(restDocs.document(
responseFields(
fieldWithPath("authenticated").description("인증 여부"),
fieldWithPath("user").description("사용자 정보").optional(),
fieldWithPath("reason").description("실패 사유 (INVALID_TOKEN)")
)
));
Expand Down
Loading
Loading