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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.gitranker.api.domain.user;

import com.gitranker.api.domain.user.dto.PublicUserResponse;
import com.gitranker.api.domain.user.dto.RegisterUserResponse;
import com.gitranker.api.domain.user.service.UserDeletionService;
import com.gitranker.api.domain.user.service.UserQueryService;
Expand Down Expand Up @@ -33,10 +34,10 @@ public class UserController {

@GetMapping("/{username}")
@Operation(summary = "Get a user's profile", description = "Returns the public Git Ranker profile for a GitHub username.")
public ApiResponse<RegisterUserResponse> getUser(
public ApiResponse<PublicUserResponse> getUser(
@PathVariable @Pattern(regexp = USERNAME_PATTERN, message = USERNAME_MESSAGE) String username
) {
RegisterUserResponse response = userQueryService.findByUsername(username);
PublicUserResponse response = userQueryService.findByUsername(username);

return ApiResponse.success(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.gitranker.api.domain.user.dto;

import com.gitranker.api.domain.log.ActivityLog;
import com.gitranker.api.domain.user.Tier;
import com.gitranker.api.domain.user.User;

import java.time.LocalDateTime;

public record PublicUserResponse(
String nodeId,
String username,
String profileImage,
LocalDateTime updatedAt,
LocalDateTime lastFullScanAt,
int totalScore,
int ranking,
Tier tier,
double percentile,
int commitCount,
int issueCount,
int prCount,
int mergedPrCount,
int reviewCount,
int diffCommitCount,
int diffIssueCount,
int diffPrCount,
int diffMergedPrCount,
int diffReviewCount
) {
public static PublicUserResponse of(User user, ActivityLog latestLog) {
return new PublicUserResponse(
user.getNodeId(),
user.getUsername(),
user.getProfileImage(),
user.getUpdatedAt(),
user.getLastFullScanAt(),
user.getTotalScore(),
user.getRanking(),
user.getTier(),
user.getPercentile(),
latestLog.getCommitCount(),
latestLog.getIssueCount(),
latestLog.getPrCount(),
latestLog.getMergedPrCount(),
latestLog.getReviewCount(),
latestLog.getDiffCommitCount(),
latestLog.getDiffIssueCount(),
latestLog.getDiffPrCount(),
latestLog.getDiffMergedPrCount(),
latestLog.getDiffReviewCount()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.gitranker.api.domain.log.ActivityLogService;
import com.gitranker.api.domain.user.User;
import com.gitranker.api.domain.user.UserRepository;
import com.gitranker.api.domain.user.dto.RegisterUserResponse;
import com.gitranker.api.domain.user.dto.PublicUserResponse;
import com.gitranker.api.global.error.ErrorType;
import com.gitranker.api.global.error.exception.BusinessException;
import com.gitranker.api.global.logging.Event;
Expand All @@ -23,7 +23,7 @@ public class UserQueryService {
private final ActivityLogService activityLogService;
private final BusinessMetrics businessMetrics;

public RegisterUserResponse findByUsername(String username) {
public PublicUserResponse findByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new BusinessException(ErrorType.USER_NOT_FOUND));

Expand All @@ -35,6 +35,6 @@ public RegisterUserResponse findByUsername(String username) {

businessMetrics.incrementProfileViews();

return RegisterUserResponse.of(user, activityLog, false);
return PublicUserResponse.of(user, activityLog);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.gitranker.api.domain.log.ActivityLogService;
import com.gitranker.api.domain.user.User;
import com.gitranker.api.domain.user.UserRepository;
import com.gitranker.api.domain.user.dto.RegisterUserResponse;
import com.gitranker.api.domain.user.dto.PublicUserResponse;
import com.gitranker.api.global.error.ErrorType;
import com.gitranker.api.global.error.exception.BusinessException;
import com.gitranker.api.global.metrics.BusinessMetrics;
Expand All @@ -15,10 +15,15 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static com.gitranker.api.support.TestFixtures.emptyActivityLog;
import static com.gitranker.api.support.TestFixtures.activityLog;
import static com.gitranker.api.support.TestFixtures.savedUser;
import static com.gitranker.api.support.TestFixtures.stats;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
Expand All @@ -39,28 +44,55 @@ class UserQueryServiceTest {
private BusinessMetrics businessMetrics;

@Test
@DisplayName("사용자가 존재하면 최신 활동 로그를 포함한 응답을 반환한다")
void returnsUserProfileWithLatestLog() {
@DisplayName("사용자가 존재하면 최신 활동 로그를 포함한 공개 응답을 반환한다")
void returnsPublicProfileWithLatestLog() {
User user = savedUser(1L, "alice");
user.updateProfile("alice", "https://images.example.com/alice-profile.png", "alice@example.com");
ActivityLog activityLog = emptyActivityLog(user);
// 카운트마다 서로 다른 값을 주어 필드 매핑이 뒤섞이지 않았는지까지 검증한다.
ActivityLog activityLog = activityLog(user, stats(11, 12, 13, 14, 15), stats(1, 2, 3, 4, 5));

when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
when(activityLogService.getLatestLog(user)).thenReturn(activityLog);

RegisterUserResponse response = userQueryService.findByUsername("alice");
PublicUserResponse response = userQueryService.findByUsername("alice");

assertThat(response.nodeId()).isEqualTo("node-alice");
assertThat(response.username()).isEqualTo("alice");
assertThat(response.userId()).isEqualTo(1L);
assertThat(response.email()).isEqualTo("alice@example.com");
assertThat(response.profileImage()).isEqualTo("https://images.example.com/alice-profile.png");
assertThat(response.role()).isEqualTo(user.getRole());
assertThat(response.commitCount()).isEqualTo(activityLog.getCommitCount());
assertThat(response.diffCommitCount()).isEqualTo(activityLog.getDiffCommitCount());
assertThat(response.isNewUser()).isFalse();
assertThat(response.profileImage()).isEqualTo("https://images.example.com/alice.png");
assertThat(response.updatedAt()).isEqualTo(user.getUpdatedAt());
assertThat(response.lastFullScanAt()).isEqualTo(user.getLastFullScanAt());
assertThat(response.totalScore()).isEqualTo(user.getTotalScore());
assertThat(response.ranking()).isEqualTo(user.getRanking());
assertThat(response.tier()).isEqualTo(user.getTier());
assertThat(response.percentile()).isEqualTo(user.getPercentile());
assertThat(response.commitCount()).isEqualTo(11);
assertThat(response.issueCount()).isEqualTo(12);
assertThat(response.prCount()).isEqualTo(13);
assertThat(response.mergedPrCount()).isEqualTo(14);
assertThat(response.reviewCount()).isEqualTo(15);
assertThat(response.diffCommitCount()).isEqualTo(1);
assertThat(response.diffIssueCount()).isEqualTo(2);
assertThat(response.diffPrCount()).isEqualTo(3);
assertThat(response.diffMergedPrCount()).isEqualTo(4);
assertThat(response.diffReviewCount()).isEqualTo(5);
verify(businessMetrics).incrementProfileViews();
}

@Test
@DisplayName("공개 응답 DTO에는 PII·내부 식별자 필드가 없고 계약상 19개 공개 필드만 포함한다")
void publicResponseExcludesPii() {
Set<String> componentNames = Arrays.stream(PublicUserResponse.class.getRecordComponents())
.map(RecordComponent::getName)
.collect(Collectors.toSet());

assertThat(componentNames).doesNotContain("userId", "githubId", "email", "role", "isNewUser");
assertThat(componentNames).containsExactlyInAnyOrder(
"nodeId", "username", "profileImage", "updatedAt", "lastFullScanAt",
"totalScore", "ranking", "tier", "percentile",
"commitCount", "issueCount", "prCount", "mergedPrCount", "reviewCount",
"diffCommitCount", "diffIssueCount", "diffPrCount", "diffMergedPrCount", "diffReviewCount"
);
}

@Test
@DisplayName("사용자가 없으면 USER_NOT_FOUND 예외가 발생한다")
void throwsWhenUserDoesNotExist() {
Expand Down
Loading