Skip to content

Dev#154

Closed
5solbin wants to merge 9 commits into
mainfrom
dev
Closed

Dev#154
5solbin wants to merge 9 commits into
mainfrom
dev

Conversation

@5solbin
Copy link
Copy Markdown
Contributor

@5solbin 5solbin commented May 22, 2026

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 이미지 업로드 기능 추가: 인증된 사용자가 JPEG, PNG, WebP, GIF 형식의 이미지를 업로드할 수 있습니다.
    • 업로드된 이미지는 공개 URL로 접근 가능하며, 최대 5MB 크기 제한이 적용됩니다.
  • Tests

    • 이미지 업로드 기능에 대한 검증 테스트 추가.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

개요

AWS S3 SDK 의존성을 추가하고 Cloudflare R2 기반 이미지 업로드 기능을 구현합니다. 설정 속성, S3 클라이언트 빈, 스토리지 서비스 계약/구현, REST API 엔드포인트, 인증 보안, 테스트를 포함합니다.

변경사항

R2 이미지 업로드 기능

레이어 / 파일(s) 요약
R2 설정 및 의존성
build.gradle, src/main/java/com/valanse/valanse/common/config/R2Properties.java, src/main/java/com/valanse/valanse/common/config/R2Config.java
AWS S3 SDK 의존성을 추가하고, Cloudflare R2 설정을 @ConfigurationProperties로 정의합니다. R2Config는 고정 리전, 자격증명, 경로 스타일 설정을 포함한 S3Client 빈을 구성합니다.
스토리지 서비스 계약 및 구현
src/main/java/com/valanse/valanse/service/StorageService/StorageService.java, src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java
uploadImage(MultipartFile, String) 계약을 정의하고, R2 구현에서 파일 크기(최대 5MB), MIME 타입(JPEG/PNG/WebP/GIF) 검증, UUID 기반 파일명 생성, 공개 URL 반환, 예외 매핑을 수행합니다.
이미지 업로드 API 엔드포인트
src/main/java/com/valanse/valanse/controller/StorageController.java, src/main/java/com/valanse/valanse/dto/Storage/ImageUploadResponse.java
POST /storage/images로 다중파트 파일을 업로드받아 StorageService로 처리하고, 공개 URL을 ImageUploadResponse DTO로 감싼 200 OK 응답을 반환합니다.
보안 설정 및 CORS
src/main/java/com/valanse/valanse/common/config/SecurityConfig.java
POST /storage/images를 인증된 사용자만 접근 가능하도록 보호합니다. CORS 허용 도메인을 backendbase.store 변형에서 https://valanserver.store로 변경합니다.
테스트 및 애플리케이션 설정
src/test/resources/application-test.yml, src/test/java/com/valanse/valanse/service/StorageService/R2StorageServiceTest.java
테스트용 R2 더미 설정(localhost 엔드포인트, 버킷, 자격증명)을 추가합니다. 테스트는 PNG 업로드 시 CDN URL 반환, S3 putObject 호출 확인, 이미지 파일 아닌 경우 ApiException 발생을 검증합니다.

이미지 업로드 시퀀스

sequenceDiagram
  participant Client
  participant StorageController
  participant R2StorageService
  participant S3Client
  participant R2Storage["Cloudflare R2"]
  
  Client->>StorageController: POST /storage/images (multipart)
  StorageController->>R2StorageService: uploadImage(file, "images")
  R2StorageService->>R2StorageService: validateImage (크기, MIME 타입)
  R2StorageService->>R2StorageService: buildObjectKey (UUID + 확장자)
  R2StorageService->>S3Client: putObject (PutObjectRequest)
  S3Client->>R2Storage: PUT object
  R2Storage-->>S3Client: success
  R2StorageService->>R2StorageService: 공개 URL 조립 (publicUrl + key)
  R2StorageService-->>StorageController: imageUrl String
  StorageController-->>Client: ImageUploadResponse (200 OK)
Loading

예상 코드 리뷰 난이도

🎯 3 (보통) | ⏱️ ~35분

관련 PR

  • ValanSee/ValanSe_Server#142: SecurityConfig의 CORS allowedOrigins 설정에서 https://valanserver.store 도메인 허용 항목을 추가/조정하는 코드 수준 중복이 있습니다.

시 🐰

AWS 클라우드 품고 와,
R2에 이미지 담아내고 🖼️
퍼블릭 URL 쏙~
인증 사용자만 쌩쌩~
업로드 기능 완성!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'Dev'는 AWS S3 의존성 추가, Cloudflare R2 설정, 이미지 업로드 기능 구현 등 변경사항의 주요 내용을 전혀 설명하지 않습니다. 'Add image upload functionality with Cloudflare R2 storage' 같은 구체적인 제목으로 변경하여 변경사항의 핵심을 명확히 표현해야 합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@5solbin 5solbin closed this May 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/main/java/com/valanse/valanse/common/config/R2Properties.java`:
- Around line 6-15: R2Properties lacks validation so null/empty values cause
ambiguous exceptions when R2Config.s3Client calls URI.create and
AwsBasicCredentials.create; add validation to enforce required fields by
annotating the class with `@Validated` (or switching to constructor binding with
`@ConstructorBinding`) and mark each required String field (accountId, endpoint,
bucket, accessKey, secretKey, publicUrl if required) with `@NotBlank/`@NotNull (or
use `@NotEmpty`) and provide clear validation messages so Spring fails fast with a
readable error on startup; ensure R2Config.s3Client continues to reference
properties.getEndpoint(), getAccessKey(), getSecretKey() but rely on validated
properties to avoid NPE/IllegalArgumentException.

In
`@src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java`:
- Around line 57-68: validateImage currently only checks file size and MIME type
(ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE) which can be spoofed; update
R2StorageService.validateImage to also verify the binary is a real image by
attempting to decode it (e.g., ImageIO.read(file.getInputStream()) and throwing
ApiException if it returns null) and, because ImageIO doesn't support all
formats (e.g., webp), add a fallback magic-byte check for JPEG/PNG/GIF/WEBP by
peeking the first bytes of file.getInputStream() (ensure the stream is
resettable or use a buffered copy) and throw ApiException on mismatch; wrap
IOExceptions into ApiException and close streams properly.

In
`@src/test/java/com/valanse/valanse/service/StorageService/R2StorageServiceTest.java`:
- Around line 50-60: Update the tests for storageService.uploadImage to assert
detailed ApiException properties and S3 interactions: in the non-image test
assert the thrown ApiException's ErrorCode and message, and verify the mocked
s3Client.putObject (or equivalent) was never invoked; add a test that stubs
s3Client.putObject to throw S3Exception and assert the service maps it to
ErrorCode.BAD_GATEWAY; add a test that stubs MultipartFile.getBytes() to throw
IOException and assert the service maps it to ErrorCode.BAD_REQUEST (use
ApiException exception = assertThrows(...) and
assertThat(exception.getErrorCode()).isEqualTo(...), and use verify(s3Client,
never()).putObject(...) to confirm no S3 call on validation failure).
- Around line 34-48: Update the R2StorageServiceTest to strengthen S3 call
verification and add boundary tests: when asserting the putObject call
(s3Client.putObject) capture the PutObjectRequest and RequestBody (or use
argument matchers) and verify the request's bucket name, key/path contains the
"images" prefix, and the PutObjectRequest's metadata/content-type equals the
file's MIME type; also assert the RequestBody/byte content length equals the
uploaded byte length. Add tests calling storageService.uploadImage for oversized
input (e.g., 11MB) and empty/null payloads asserting ApiException is thrown (use
assertThrows(ApiException.class, ...)) and ensure the existing positive test
still verifies URL format and that putObject was invoked with the correct
captured values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c0e5a76-c18c-4605-8b13-ebc2917d9bc9

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2601b and 8d94cd1.

📒 Files selected for processing (10)
  • build.gradle
  • src/main/java/com/valanse/valanse/common/config/R2Config.java
  • src/main/java/com/valanse/valanse/common/config/R2Properties.java
  • src/main/java/com/valanse/valanse/common/config/SecurityConfig.java
  • src/main/java/com/valanse/valanse/controller/StorageController.java
  • src/main/java/com/valanse/valanse/dto/Storage/ImageUploadResponse.java
  • src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java
  • src/main/java/com/valanse/valanse/service/StorageService/StorageService.java
  • src/test/java/com/valanse/valanse/service/StorageService/R2StorageServiceTest.java
  • src/test/resources/application-test.yml

Comment on lines +6 to +15
@Component
@ConfigurationProperties(prefix = "cloudflare.r2")
public class R2Properties {

private String accountId;
private String endpoint;
private String bucket;
private String accessKey;
private String secretKey;
private String publicUrl;
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# R2Properties의 필수값 검증 어노테이션 적용 여부 확인
fd -i R2Properties.java src | xargs -I{} sh -c '
  echo "== {} ==";
  rg -n "`@ConfigurationProperties`|`@Validated`|`@NotBlank`|private String (accountId|endpoint|bucket|accessKey|secretKey|publicUrl)" "{}"
'

Repository: ValanSee/ValanSe_Server

Length of output: 386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) R2Properties.java 전체 확인
echo "---- R2Properties.java (head/tail with line numbers) ----"
FILE="src/main/java/com/valanse/valanse/common/config/R2Properties.java"
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '1,220p'
else
  echo "Missing $FILE"
fi

# 2) 이 프로퍼티(클래스)를 참조하는 곳 확인
echo
echo "---- Usages of R2Properties ----"
rg -n "R2Properties" src || true

# 3) 공용 URL/endpoint/accountId 등을 실제로 쓰는 곳에서 URI.create(등) 여부 확인
echo
echo "---- URI.create usages around R2/endpoint/publicUrl ----"
rg -n "URI\.create|publicUrl|endpoint|accountId|secretKey|accessKey" src/main/java/com/valanse/valanse || true

Repository: ValanSee/ValanSe_Server

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/java/com/valanse/valanse/common/config/R2Properties.java"

echo "---- R2Properties.java (line-numbered) ----"
if [ -f "$FILE" ]; then
  cat -n "$FILE" | sed -n '1,220p'
else
  echo "Missing $FILE"
fi

echo
echo "---- Usages of R2Properties ----"
rg -n "R2Properties" src || true

echo
echo "---- URI.create usages and related fields ----"
rg -n "URI\.create" src || true
rg -n "publicUrl|endpoint|accountId|accessKey|secretKey|bucket" src/main/java/com/valanse/valanse || true

Repository: ValanSee/ValanSe_Server

Length of output: 6433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# show relevant sections
echo "---- R2Config.java (key parts) ----"
FILE="src/main/java/com/valanse/valanse/common/config/R2Config.java"
if [ -f "$FILE" ]; then
  sed -n '1,120p' "$FILE" | nl -ba
else
  echo "Missing $FILE"
fi

echo
echo "---- R2StorageService.java (key parts) ----"
FILE="src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java"
if [ -f "$FILE" ]; then
  sed -n '1,120p' "$FILE" | nl -ba
else
  echo "Missing $FILE"
fi

Repository: ValanSee/ValanSe_Server

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- R2Config.java (selected lines) ----"
FILE="src/main/java/com/valanse/valanse/common/config/R2Config.java"
if [ -f "$FILE" ]; then
  cat -n "$FILE" | sed -n '1,140p'
else
  echo "Missing $FILE"
fi

echo
echo "---- R2StorageService.java (selected lines) ----"
FILE="src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java"
if [ -f "$FILE" ]; then
  cat -n "$FILE" | sed -n '1,140p'
else
  echo "Missing $FILE"
fi

Repository: ValanSee/ValanSe_Server

Length of output: 4918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "import (jakarta\.validation|javax\.validation)\.constraints\.NotBlank|`@NotBlank`|import org\.springframework\.validation\.annotation\.Validated|`@Validated`" src || true

Repository: ValanSee/ValanSe_Server

Length of output: 49


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- root build files ----"
ls -la

# pom.xml / build.gradle 계열 확인
rg -n "spring-boot-starter-validation|validation-api|jakarta\.validation|javax\.validation" -S . || true

Repository: ValanSee/ValanSe_Server

Length of output: 1136


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- build.gradle (validation-related section) ----"
sed -n '1,120p' build.gradle | cat -n

echo
echo "---- jakarta/javax validation imports in src ----"
rg -n "jakarta\.validation|javax\.validation" src || true

echo
echo "---- Spring `@Validated` usage in src ----"
rg -n "org\.springframework\.validation\.annotation\.Validated|`@Validated`" src || true

Repository: ValanSee/ValanSe_Server

Length of output: 3036


R2 필수 설정값을 부팅 시점에 검증해 주세요.

  • R2Properties는 @ConfigurationProperties만 있고 @Validated/@notblank 제약이 없어 null/빈 문자열이 그대로 바인딩됩니다.
  • 실제로 R2Config.s3Client에서 URI.create(properties.getEndpoint())와 AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())를 즉시 호출하므로 null/형식 불일치 시 부팅 중 NPE/IllegalArgumentException 같은 모호한 예외로 실패할 수 있습니다.
제안 diff
+import jakarta.validation.constraints.NotBlank;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.stereotype.Component;

 `@Component`
 `@ConfigurationProperties`(prefix = "cloudflare.r2")
+@Validated
 public class R2Properties {

+    `@NotBlank`
     private String accountId;
+    `@NotBlank`
     private String endpoint;
+    `@NotBlank`
     private String bucket;
+    `@NotBlank`
     private String accessKey;
+    `@NotBlank`
     private String secretKey;
+    `@NotBlank`
     private String publicUrl;
🤖 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 `@src/main/java/com/valanse/valanse/common/config/R2Properties.java` around
lines 6 - 15, R2Properties lacks validation so null/empty values cause ambiguous
exceptions when R2Config.s3Client calls URI.create and
AwsBasicCredentials.create; add validation to enforce required fields by
annotating the class with `@Validated` (or switching to constructor binding with
`@ConstructorBinding`) and mark each required String field (accountId, endpoint,
bucket, accessKey, secretKey, publicUrl if required) with `@NotBlank/`@NotNull (or
use `@NotEmpty`) and provide clear validation messages so Spring fails fast with a
readable error on startup; ensure R2Config.s3Client continues to reference
properties.getEndpoint(), getAccessKey(), getSecretKey() but rely on validated
properties to avoid NPE/IllegalArgumentException.

Comment on lines +57 to +68
private void validateImage(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new ApiException("업로드할 이미지 파일을 선택해주세요.", HttpStatus.BAD_REQUEST);
}

if (file.getSize() > MAX_IMAGE_SIZE) {
throw new ApiException("이미지는 5MB 이하만 업로드할 수 있습니다.", HttpStatus.BAD_REQUEST);
}

if (!ALLOWED_IMAGE_TYPES.contains(file.getContentType())) {
throw new ApiException("jpg, png, webp, gif 이미지 파일만 업로드할 수 있습니다.", HttpStatus.BAD_REQUEST);
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# MIME-only 검증 여부 확인 (바이너리 시그니처 검증 코드 유무 탐색)
fd -i R2StorageService.java src | xargs -I{} sh -c '
  echo "== {} ==";
  rg -n "ALLOWED_IMAGE_TYPES|getContentType|ImageIO|Tika|magic" "{}"
'

Repository: ValanSee/ValanSe_Server

Length of output: 348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java"

echo "===== validateImage() snippet ====="
nl -ba "$FILE" | sed -n '40,110p'

echo "===== allowed types set ====="
nl -ba "$FILE" | sed -n '15,55p'

echo "===== repo-wide image signature validation hints ====="
rg -n "ImageIO|Tika|magic|contentType\\(file\\.getContentType\\)|getInputStream\\(" src/main/java || true

echo "===== validateImage 호출/흐름 ====="
rg -n "validateImage\\(" -S src/main/java || true

Repository: ValanSee/ValanSe_Server

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java"

echo "===== validateImage() snippet (line 40-110) ====="
awk 'NR>=40 && NR<=110 {printf "%5d:%s\n", NR, $0}' "$FILE" || true

echo "===== allowed types set (line 15-55) ====="
awk 'NR>=15 && NR<=55 {printf "%5d:%s\n", NR, $0}' "$FILE" || true

echo "===== repo-wide image signature validation hints ====="
rg -n "ImageIO|Tika|magic|getInputStream\\(" src/main/java || true
rg -n "contentType\\(file\\.getContentType\\)|getContentType\\(\\)" src/main/java || true

echo "===== validateImage 호출/흐름 ====="
rg -n "validateImage\\(" src/main/java || true

Repository: ValanSee/ValanSe_Server

Length of output: 4568


MIME 타입만으로는 이미지 검증이 부족합니다.
src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java의 validateImage()file.getContentType()와 크기만 검사하고(라인 66-68), 매직 바이트/디코딩 가능 여부 같은 바이너리 시그니처 검증이 없습니다. 따라서 Content-Type을 위조한 비이미지 업로드가 통과할 수 있으니, 최소한 ImageIO.read(...) 또는 매직 바이트 기반 검사로 유효한 이미지인지 확인을 추가해 주세요.

제안 diff
+import javax.imageio.ImageIO;
+import java.io.InputStream;
@@
     private void validateImage(MultipartFile file) {
@@
         if (!ALLOWED_IMAGE_TYPES.contains(file.getContentType())) {
             throw new ApiException("jpg, png, webp, gif 이미지 파일만 업로드할 수 있습니다.", HttpStatus.BAD_REQUEST);
         }
+
+        try (InputStream in = file.getInputStream()) {
+            if (ImageIO.read(in) == null) {
+                throw new ApiException("유효한 이미지 파일이 아닙니다.", HttpStatus.BAD_REQUEST);
+            }
+        } catch (IOException e) {
+            throw new ApiException("이미지 파일을 읽을 수 없습니다.", HttpStatus.BAD_REQUEST);
+        }
     }
🤖 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
`@src/main/java/com/valanse/valanse/service/StorageService/R2StorageService.java`
around lines 57 - 68, validateImage currently only checks file size and MIME
type (ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE) which can be spoofed; update
R2StorageService.validateImage to also verify the binary is a real image by
attempting to decode it (e.g., ImageIO.read(file.getInputStream()) and throwing
ApiException if it returns null) and, because ImageIO doesn't support all
formats (e.g., webp), add a fallback magic-byte check for JPEG/PNG/GIF/WEBP by
peeking the first bytes of file.getInputStream() (ensure the stream is
resettable or use a buffered copy) and throw ApiException on mismatch; wrap
IOExceptions into ApiException and close streams properly.

Comment on lines +34 to +48
@Test
void 이미지를_R2에_업로드하고_공개_URL을_반환한다() {
MockMultipartFile file = new MockMultipartFile(
"file",
"sample.png",
"image/png",
"image".getBytes()
);

String imageUrl = storageService.uploadImage(file, "images");

assertThat(imageUrl).startsWith("https://cdn.example.com/images/");
assertThat(imageUrl).endsWith(".png");
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

S3 호출 검증이 불충분하고 파일 크기 검증 테스트가 누락되었습니다.

현재 테스트는 any() 매처만 사용하여 putObject 호출 여부만 확인하고 있습니다. 컨텍스트에 따르면 서비스는 파일 크기 및 MIME 타입을 검증해야 하지만, 이에 대한 테스트가 없습니다.

다음 개선사항을 권장합니다:

  1. putObject 호출 시 올바른 버킷명, 키(경로), Content-Type이 전달되는지 검증
  2. 파일 크기 제한 초과 시 예외 발생 테스트 추가
  3. 빈 파일, null 파일 등 경계 케이스 테스트 추가
🧪 검증 강화 예시
 `@Test`
 void 이미지를_R2에_업로드하고_공개_URL을_반환한다() {
     MockMultipartFile file = new MockMultipartFile(
             "file",
             "sample.png",
             "image/png",
             "image".getBytes()
     );

     String imageUrl = storageService.uploadImage(file, "images");

     assertThat(imageUrl).startsWith("https://cdn.example.com/images/");
     assertThat(imageUrl).endsWith(".png");
-    verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
+    
+    ArgumentCaptor<PutObjectRequest> requestCaptor = ArgumentCaptor.forClass(PutObjectRequest.class);
+    verify(s3Client).putObject(requestCaptor.capture(), any(RequestBody.class));
+    
+    PutObjectRequest request = requestCaptor.getValue();
+    assertThat(request.bucket()).isEqualTo("test-bucket");
+    assertThat(request.key()).startsWith("images/");
+    assertThat(request.contentType()).isEqualTo("image/png");
 }

추가 테스트 케이스:

`@Test`
void 파일_크기가_제한을_초과하면_예외가_발생한다() {
    byte[] largeContent = new byte[11 * 1024 * 1024]; // 11MB
    MockMultipartFile file = new MockMultipartFile(
            "file",
            "large.png",
            "image/png",
            largeContent
    );

    assertThrows(ApiException.class, () -> storageService.uploadImage(file, "images"));
}

`@Test`
void 빈_파일은_업로드하지_않는다() {
    MockMultipartFile file = new MockMultipartFile(
            "file",
            "empty.png",
            "image/png",
            new byte[0]
    );

    assertThrows(ApiException.class, () -> storageService.uploadImage(file, "images"));
}
🤖 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
`@src/test/java/com/valanse/valanse/service/StorageService/R2StorageServiceTest.java`
around lines 34 - 48, Update the R2StorageServiceTest to strengthen S3 call
verification and add boundary tests: when asserting the putObject call
(s3Client.putObject) capture the PutObjectRequest and RequestBody (or use
argument matchers) and verify the request's bucket name, key/path contains the
"images" prefix, and the PutObjectRequest's metadata/content-type equals the
file's MIME type; also assert the RequestBody/byte content length equals the
uploaded byte length. Add tests calling storageService.uploadImage for oversized
input (e.g., 11MB) and empty/null payloads asserting ApiException is thrown (use
assertThrows(ApiException.class, ...)) and ensure the existing positive test
still verifies URL format and that putObject was invoked with the correct
captured values.

Comment on lines +50 to +60
@Test
void 이미지가_아닌_파일은_업로드하지_않는다() {
MockMultipartFile file = new MockMultipartFile(
"file",
"sample.txt",
"text/plain",
"text".getBytes()
);

assertThrows(ApiException.class, () -> storageService.uploadImage(file, "images"));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

예외 검증이 불충분하고 예외 매핑 테스트가 누락되었습니다.

현재 테스트는 ApiException이 발생하는지만 확인하고, 예외의 상세 정보(에러 코드, 메시지)나 S3 클라이언트가 호출되지 않았는지 검증하지 않습니다. 컨텍스트에 따르면 서비스는 IOException → BAD_REQUEST, S3Exception → BAD_GATEWAY로 매핑해야 하지만 이에 대한 테스트가 없습니다.

다음 개선사항을 권장합니다:

  1. ApiException의 에러 코드 및 메시지 검증
  2. 검증 실패 시 S3 호출이 발생하지 않았는지 확인
  3. S3Exception 발생 시 BAD_GATEWAY 매핑 테스트 추가
  4. IOException 발생 시 BAD_REQUEST 매핑 테스트 추가
🧪 예외 검증 강화 및 추가 테스트 예시
 `@Test`
 void 이미지가_아닌_파일은_업로드하지_않는다() {
     MockMultipartFile file = new MockMultipartFile(
             "file",
             "sample.txt",
             "text/plain",
             "text".getBytes()
     );

-    assertThrows(ApiException.class, () -> storageService.uploadImage(file, "images"));
+    ApiException exception = assertThrows(ApiException.class, 
+        () -> storageService.uploadImage(file, "images"));
+    
+    assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.BAD_REQUEST);
+    verify(s3Client, never()).putObject(any(PutObjectRequest.class), any(RequestBody.class));
 }

추가 테스트 케이스:

`@Test`
void S3_업로드_실패_시_BAD_GATEWAY_예외가_발생한다() {
    MockMultipartFile file = new MockMultipartFile(
            "file",
            "sample.png",
            "image/png",
            "image".getBytes()
    );
    
    when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
        .thenThrow(S3Exception.builder().message("S3 error").build());

    ApiException exception = assertThrows(ApiException.class, 
        () -> storageService.uploadImage(file, "images"));
    
    assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.BAD_GATEWAY);
}

`@Test`
void IOException_발생_시_BAD_REQUEST_예외가_발생한다() throws Exception {
    MultipartFile file = mock(MultipartFile.class);
    when(file.getContentType()).thenReturn("image/png");
    when(file.getOriginalFilename()).thenReturn("sample.png");
    when(file.getSize()).thenReturn(100L);
    when(file.getBytes()).thenThrow(new IOException("Read error"));

    ApiException exception = assertThrows(ApiException.class, 
        () -> storageService.uploadImage(file, "images"));
    
    assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.BAD_REQUEST);
}
🤖 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
`@src/test/java/com/valanse/valanse/service/StorageService/R2StorageServiceTest.java`
around lines 50 - 60, Update the tests for storageService.uploadImage to assert
detailed ApiException properties and S3 interactions: in the non-image test
assert the thrown ApiException's ErrorCode and message, and verify the mocked
s3Client.putObject (or equivalent) was never invoked; add a test that stubs
s3Client.putObject to throw S3Exception and assert the service maps it to
ErrorCode.BAD_GATEWAY; add a test that stubs MultipartFile.getBytes() to throw
IOException and assert the service maps it to ErrorCode.BAD_REQUEST (use
ApiException exception = assertThrows(...) and
assertThat(exception.getErrorCode()).isEqualTo(...), and use verify(s3Client,
never()).putObject(...) to confirm no S3 call on validation failure).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant