Skip to content

test: 아바타 업로드 Feature 테스트와 UploadStorage seam - #102

Merged
nambak merged 2 commits into
mainfrom
test/avatar-upload
Jul 22, 2026
Merged

test: 아바타 업로드 Feature 테스트와 UploadStorage seam#102
nambak merged 2 commits into
mainfrom
test/avatar-upload

Conversation

@nambak

@nambak nambak commented Jul 22, 2026

Copy link
Copy Markdown
Owner

이슈 #95 입니다. Profile::saveUploadedAvatar() 경로를 자동 테스트로 덮었습니다.

이슈 본문의 전제가 일부 틀렸습니다

임시 프로브 테스트로 실제 확인했습니다(확인 후 삭제).

[PROBE] getFile: CodeIgniter\HTTP\Files\UploadedFile
[PROBE] getError=0 getSize=70 isValid=false
[PROBE] validation=true errors=[]
[PROBE] move threw HTTPException: The original file is not a valid file.
  1. $_FILES 주입은 공식 API로 됩니다service('superglobals')->setFilesArray(). FileCollection::populateFiles() 가 이 서비스를 거치므로 정상적인 UploadedFile 이 만들어집니다.
  2. 검증 규칙은 CLI 에서 통과합니다. is_image·mime_in·max_sizegetError()·getSize()·mime 만 보고 isValid() 를 쓰지 않습니다. isValid() 에 의존하는 건 uploaded[] 규칙뿐인데 이 프로젝트는 안 씁니다.
  3. 실제로 막히는 건 $file->move() 한 곳뿐입니다.

즉 테스트 불가 영역은 업로드 경로 전체가 아니라 프레임워크 코드 한 줄이었습니다.

접근

그 한 줄만 App\Libraries\UploadStorage 뒤로 보내고, 테스트에서 Services::injectMock() 으로 바꿔 낍니다.

-        $dir  = WRITEPATH . 'uploads';
-        $name = $file->getRandomName();
-        $file->move($dir, $name);
-
-        return $name;
+        return service('uploadStorage')->store($file);

검토했다가 버린 대안 두 가지:

  • UploadedFile 서브클래스 주입FileCollection::createFileObject()new UploadedFile(...) 을 하드코딩해 FileCollection·IncomingRequest 까지 갈아끼워야 합니다. 프레임워크 내부 결합이라 업그레이드에 취약합니다.
  • uopz 확장으로 내장 함수 대체 — 프로덕션 코드는 안 건드리지만 로컬·CI 양쪽에 PHP 확장이 필요해집니다. 강좌용 저장소에 진입 장벽을 만들 이유가 없습니다.

적용 범위는 아바타만으로 했습니다. 글 대표 이미지(Posts.php:305)도 같은 모양이지만, 테스트 없는 리팩터링을 남기지 않으려고 그쪽 테스트와 함께 후속 이슈에서 바꾸겠습니다.

가짜 저장기가 실제 파일을 만듭니다

FakeUploadStoragemove_uploaded_file()copy() 로 바꾸고, 파일명은 진짜 getRandomName() 을 씁니다. 이름만 돌려주는 가짜였다면 "새 파일이 저장된 뒤 옛 파일을 지운다"는 순서나 deleteAvatarFile()is_file() 분기가 통과하는 척만 하게 됩니다.

테스트 4건

케이스 지키는 것
업로드 성공 avatar 컬럼 = 저장 파일명, 파일이 실제로 생성됨
기존 아바타 교체 옛 파일이 지워지고 컬럼이 새 이름으로 — 순서 보장
텍스트 파일이 image/png 라고 위장 리다이렉트 + 기존 아바타·사용자명 유지 + 저장 시도 없음
용량 초과(신고 크기 3MB) 위와 동일

세 번째는 mime_in 이 클라이언트가 신고한 타입이 아니라 실제 감지된 mime(getMimeType())을 본다는 점을 검증합니다. 네 번째는 getSize()$_FILES['size'] 를 우선 쓰므로 큰 파일을 만들지 않았습니다 — 실제 업로드에서도 PHP 가 전송 크기를 그 자리에 넣습니다.

확인

274/274 통과(기존 270 + 신규 4). 뮤테이션으로 각 테스트가 자기 결함에만 반응하는 것을 확인했습니다.

뮤테이션 실패한 테스트
옛 파일 삭제 제거 교체 케이스만
is_image+mime_in 제거 위장 파일 케이스만
max_size 제거 용량 초과 케이스만
저장 안 하고 이름만 반환 업로드 성공 + 교체

전체 스위트에서 처음엔 5건이 깨졌습니다. setFilesArray()$_FILES 전역까지 덮어써서(Superglobals.php:408) 주입값이 뒤 테스트로 샜고, 그쪽이 이미 지워진 임시 파일을 업로드로 인식했습니다. tearDown 에서 전역을 비우고 서비스를 리셋해 막았습니다.

덮지 못하는 것

move_uploaded_file() 실제 호출 한 줄입니다. PR #70 의 curl 종단 검증으로 확인했고 앞으로도 수동 영역입니다. 업로드가 전부 테스트된다고 오해하지 않도록 여기에 적어 둡니다.

Closes #95

Summary by CodeRabbit

  • 새로운 기능
    • 프로필 아바타 업로드를 전용 업로드 저장 기능으로 일원화했습니다.
    • 업로드 결과에 따라 저장 파일명을 프로필에 반영합니다.
  • 버그 수정
    • 아바타 교체 시 이전 파일을 삭제합니다.
    • 이미지로 위장된 파일 또는 용량 초과 파일은 저장 및 반영을 거부합니다.
  • 테스트
    • 업로드/교체/거부/용량 제한 동작을 검증하는 기능 테스트를 추가했습니다.

UploadedFile::move() 가 is_uploaded_file()·move_uploaded_file() 에 직접
의존해 CLI 테스트로 태울 수 없었다. 저장 동작 한 줄만 UploadStorage 로
빼고 테스트에서 바꿔 끼워, 나머지 업로드 로직을 전부 Feature 테스트로
덮는다.

- 검증 실패 분기·랜덤 파일명·옛 파일 삭제 순서·avatar 컬럼 갱신까지 커버
- 가짜 저장기도 copy() 로 실제 파일을 만든다. 이름만 돌려주면 파일 존재를
  전제로 한 뒷단 로직이 통과하는 척만 하게 된다
- 덮지 못하는 것은 move_uploaded_file() 호출 한 줄(프레임워크 코드)

Closes #95

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 141e254a-891d-4f87-82fc-18bedca01006

📥 Commits

Reviewing files that changed from the base of the PR and between 407a79a and d01986a.

📒 Files selected for processing (1)
  • tests/Feature/ProfileAvatarUploadTest.php

Walkthrough

프로필 아바타 저장을 UploadStorage 서비스로 분리하고, 기본 저장소와 테스트용 저장소를 추가했다. Feature 테스트는 저장, 교체, 이미지 검증, 파일 크기 제한을 확인한다.

Changes

아바타 업로드 저장소

Layer / File(s) Summary
저장소 서비스 연결
app/Libraries/UploadStorage.php, app/Config/Services.php, app/Controllers/Profile.php
UploadStorage가 임의 파일명으로 파일을 저장하며, ProfileuploadStorage 서비스에 저장을 위임한다.
아바타 업로드 Feature 검증
tests/_support/Libraries/FakeUploadStorage.php, tests/Feature/ProfileAvatarUploadTest.php
테스트 더블을 사용해 저장, 기존 파일 교체, 비이미지 파일 및 크기 초과 업로드 거부를 검증한다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Profile
  participant UploadStorage
  participant UploadDirectory
  Browser->>Profile: 아바타 업로드 요청
  Profile->>UploadStorage: UploadedFile 저장 요청
  UploadStorage->>UploadDirectory: 임의 이름으로 파일 이동
  UploadDirectory-->>UploadStorage: 저장 파일명 반환
  UploadStorage-->>Profile: 저장 파일명 반환
  Profile-->>Browser: 프로필 응답
Loading

Possibly related issues

  • nambak/ci4blog issue 103 — 동일한 UploadStorage 연결과 Feature 테스트 패턴을 게시물 이미지 업로드에 적용하는 내용이다.

Possibly related PRs

  • nambak/ci4blog#70 — 프로필 아바타 업로드 흐름을 다루며, 이 변경은 저장 로직을 서비스 위임 방식으로 전환한다.

Poem

토끼가 파일을 살포시 올리고
저장소는 새 이름을 붙였네
낡은 아바타는 조용히 떠나고
가짜 PNG는 문밖에 멈췄네
큰 파일도 굴엔 못 들어와
깡충, 테스트가 확인했네

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 아바타 업로드 Feature 테스트와 UploadStorage 분리를 정확히 요약합니다.
Linked Issues check ✅ Passed 이슈 #95의 핵심인 Profile::saveUploadedAvatar 경로의 Feature 테스트와 업로드 저장 seam 분리가 반영되었습니다.
Out of Scope Changes check ✅ Passed 변경 내용은 업로드 저장 분리, 테스트 더블, 관련 Feature 테스트로 모두 PR 목적과 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 test/avatar-upload

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@tests/Feature/ProfileAvatarUploadTest.php`:
- Around line 87-89: Update makeTempPng() and
testRejectsNonImageDisguisedAsPng() to use tempnam()’s returned path directly,
without appending .png; keep the desired PNG filename supplied separately
through attach()’s name argument.
- Around line 46-47: Update the test teardown to call
Services::resetSingle('uploadStorage') after the FakeUploadStorage mock is
injected, ensuring the shared uploadStorage instance is cleared between tests.
🪄 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: e8ac6b6d-b8b3-4111-87d7-c9425f4fdbbd

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbece3 and 407a79a.

📒 Files selected for processing (5)
  • app/Config/Services.php
  • app/Controllers/Profile.php
  • app/Libraries/UploadStorage.php
  • tests/Feature/ProfileAvatarUploadTest.php
  • tests/_support/Libraries/FakeUploadStorage.php

Comment thread tests/Feature/ProfileAvatarUploadTest.php Outdated
Comment thread tests/Feature/ProfileAvatarUploadTest.php Outdated
CodeRabbit 지적 2건. 둘 다 실증하고 고쳤다.

1) tempnam() 반환값에 .png 를 덧붙여 원본 임시 파일이 추적되지 않은 채
   남았다. 반환값을 그대로 쓴다(업로드 파일명은 attach() 의 $name 이 정한다).

2) 주입한 가짜 저장기가 다른 테스트 클래스까지 살아남았다. 프로브로 확인:
   ZzLeakProbe 가 FakeUploadStorage 를 받았다.

   원인은 프레임워크의 비대칭이다. injectMock() 은 $instances 에 준 이름
   그대로, $mocks 에는 소문자로 넣는데 resetSingle() 은 소문자 키만 지운다.
   그래서 $instances['uploadStorage'] 는 공개 API 로 지울 수 없고,
   service() 는 파라미터가 없을 때 Services::get() 으로 그 $instances 를
   그대로 읽는다. 소문자 이름으로 주입해 두 키를 맞췄다.

   이 사고는 조용하다 — 다른 테스트가 업로드를 안 하면 아무도 안 깨진다.
   그래서 방지망 테스트를 남겼고, camelCase 로 되돌리면 그 테스트만 깨진다.

275/275. 뮤테이션 4건 재확인, 임시 파일 잔여 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nambak

nambak commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

리뷰 2건 모두 반영했습니다(d01986a). 각각 실제로 성립하는지 확인한 뒤 고쳤습니다.

1) tempnam() 반환값에 .png 덧붙임 — 실재했습니다. tempnam() 이 먼저 만든 원본이 추적되지 않은 채 남습니다. 반환값을 그대로 쓰도록 고쳤고, 업로드 파일명은 원래대로 attach()$name 이 정합니다. 대상 디렉터리에서 실행 전후 개수가 46 → 46 으로 새 파일이 생기지 않는 것을 확인했고, 이전 실행이 남긴 46개는 치웠습니다.

2) 가짜 저장기 누수 — 이것도 실재했습니다. 임시 프로브 테스트를 뒤에 붙여 실증했습니다.

-'App\Libraries\UploadStorage'
+'Tests\Support\Libraries\FakeUploadStorage'

다만 제안하신 resetSingle('uploadStorage') 만으로는 안 지워집니다. 프레임워크에 비대칭이 있습니다.

  • injectMock()$instances[$name]준 이름 그대로, $mocks[strtolower($name)] 에 소문자로 넣습니다 (BaseService.php:448)
  • resetSingle() 은 이름을 소문자로 바꾼 뒤 지웁니다 → $instances['uploadstorage'] 만 지워지고 $instances['uploadStorage'] 는 남습니다
  • service('uploadStorage') 는 파라미터가 없으면 Services::get() 으로 가고, 이건 $instances원래 대소문자로 조회합니다 (Common.php:1081)

그래서 camelCase 로 주입하면 공개 API 로는 치울 방법이 없습니다. 소문자 이름으로 주입해 두 키를 맞췄습니다.

이 사고는 조용합니다 — 다른 테스트가 업로드를 안 하면 아무도 안 깨집니다. 그래서 testFakeStorageIsRegisteredUnderAResettableKey 를 방지망으로 남겼고, 주입 이름을 camelCase 로 되돌리면 그 테스트만 깨지는 것을 확인했습니다.

275/275 통과. 기존 뮤테이션 4건도 재확인했습니다.

@nambak

nambak commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nambak
nambak merged commit 7e0f72b into main Jul 22, 2026
1 check passed
@nambak
nambak deleted the test/avatar-upload branch July 22, 2026 05:55
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.

아바타 실제 업로드 Feature 테스트 (CI4 업로드 함수 제약 조사 포함)

1 participant