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
Expand Up @@ -18,7 +18,10 @@ public class InternalGithubTokenService {
private final GithubTokenCipher tokenCipher;

public String fetchPlainAccessToken(Long userId) {
User user = userRepository.findById(userId)
// 탈퇴한 계정의 토큰은 위임하지 않는다. 탈퇴 시 토큰을 지우므로(User.withdraw) 보통은
// hasGithubLink 에서 걸리지만, 그건 "값이 비어 있어서" 막히는 것이라 데이터 상태에
// 기대는 방어다. 삭제 여부로 먼저 막아 상태와 무관하게 닫는다.
User user = userRepository.findByIdAndDeletedFalse(userId)
.orElseThrow(() -> new DomainException(ApiErrorCode.USER_NOT_FOUND));
// Google 로 가입한 계정은 GitHub 토큰이 없다. 그대로 복호화로 넘기면 NPE 가 500 으로
// 새어나가므로, 무엇이 부족한지 말해 주는 도메인 에러로 바꾼다.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- V28 은 탈퇴 시 GitHub 토큰을 비울 수 있도록 CHECK 제약을 완화하기만 했다. 그 시점에
-- **이미 탈퇴해 있던** 사용자들의 토큰은 그대로 남아 있다 — #198 은 이후 탈퇴만 처리한다.
--
-- 그런데 "떠난 사용자의 repo 스코프 자격증명을 무기한 보관하지 않는다"는 목적에서 보면
-- 그 사람들이 바로 그 대상이다. 이미 탈퇴했으니 앞으로 User.withdraw() 가 불릴 일도 없어
-- 백필하지 않으면 영원히 남는다.
UPDATE users
SET encrypted_github_access_token = NULL
WHERE is_deleted = TRUE
AND encrypted_github_access_token IS NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.stackup.stackup.github.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;

import com.stackup.stackup.common.exception.ApiErrorCode;
import com.stackup.stackup.common.exception.DomainException;
import com.stackup.stackup.github.infrastructure.GithubTokenCipher;
import com.stackup.stackup.user.domain.User;
import com.stackup.stackup.user.domain.UserRepository;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

/**
* AI 서버가 레포 분석 시점에 위임받는 GitHub access token 은 `repo` 스코프다 —
* 비공개 레포까지 읽을 수 있는 살아있는 자격증명이라 위임 조건이 좁아야 한다.
*/
@ExtendWith(MockitoExtension.class)
class InternalGithubTokenServiceTest {

@Mock UserRepository userRepository;
@Mock GithubTokenCipher tokenCipher;
@InjectMocks InternalGithubTokenService service;

@Test
void returnsDecryptedTokenForActiveUser() {
User user = User.createGithubUser(1L, "u", null, null, "enc");
when(userRepository.findByIdAndDeletedFalse(1L)).thenReturn(Optional.of(user));
when(tokenCipher.decrypt("enc")).thenReturn("gho_plain");

assertThat(service.fetchPlainAccessToken(1L)).isEqualTo("gho_plain");
}

// 탈퇴한 계정은 삭제 여부에서 먼저 막는다. 토큰이 비어 있어서 막히는 것에만 기대면
// 백필 전 데이터·향후 실수에 그대로 뚫린다.
@Test
void refusesWithdrawnUserEvenIfTokenRowStillPresent() {
when(userRepository.findByIdAndDeletedFalse(1L)).thenReturn(Optional.empty());

assertThatThrownBy(() -> service.fetchPlainAccessToken(1L))
.isInstanceOf(DomainException.class)
.extracting(e -> ((DomainException) e).getErrorCode())
.isEqualTo(ApiErrorCode.USER_NOT_FOUND);
}

// Google 로 가입한 계정은 GitHub 토큰이 없다 — NPE 가 500 으로 새지 않게 도메인 에러로.
@Test
void refusesGoogleOnlyAccountWithDomainError() {
User google = User.createGoogleUser("g-1", "u", null, null);
when(userRepository.findByIdAndDeletedFalse(1L)).thenReturn(Optional.of(google));

assertThatThrownBy(() -> service.fetchPlainAccessToken(1L))
.isInstanceOf(DomainException.class)
.extracting(e -> ((DomainException) e).getErrorCode())
.isEqualTo(ApiErrorCode.AUTH_GITHUB_NOT_LINKED);
}
}
Loading