-
Notifications
You must be signed in to change notification settings - Fork 4
[cicd-80/e2e] GitHub Actions E2E 테스트 워크플로우 추가 #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
4ac73b2
docs: E2E 테스트 케이스 도메인별 초안과 작업 원본을 llm-wiki에 보존
hm1n 3e9319e
docs: E2E 테스트 케이스 관리 문서 평가와 DB 정리 결과를 기록
hm1n a78c0c7
refactor: 엔빵 카드와 참여자 카드에 테스트용 data-testid 추가
hm1n 75e6acb
test: E2E 우선순위 P0 15건을 Playwright 테스트로 구현
hm1n 38c53a4
chore: e2e 디렉터리에 no-floating-promises 규칙 적용
hm1n 7bc4ee9
docs: 정산 기록 갱신 오류 재전파 사례를 llm-wiki에 보존
hm1n 40bdb63
chore: develop의 gh-commit 스킬을 브랜치에 반영
hm1n 3dc3ada
fix: 테스트 계정 tag가 겹치면 다른 값으로 다시 시도
hm1n cdbf753
test: E2E 뷰포트를 모바일과 PC로 나누고 CI는 빌드 서버로 실행
hm1n 676e418
cicd: E2E 테스트 GitHub Actions 워크플로 추가
hm1n 22337b8
Merge remote-tracking branch 'origin/develop' into hm1n/e2e-test-case
hm1n c829d67
cicd: E2E 워크플로의 액션 버전을 현재 메이저로 올림
hm1n File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| name: CI E2E test | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - develop | ||
| - main | ||
| paths-ignore: | ||
| - '**.md' | ||
| - 'llm-wiki/**' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| # 같은 PR에 새 커밋이 오면 이전 실행을 취소해 러너 시간을 아낀다. | ||
| concurrency: | ||
| group: e2e-${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| e2e: | ||
| name: E2E (${{ matrix.project }}) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
|
|
||
| strategy: | ||
| # 한쪽 뷰포트가 깨져도 나머지 결과를 봐야 원인 범위를 좁힐 수 있다. | ||
| fail-fast: false | ||
| matrix: | ||
| # 서비스가 모바일 우선이라 평소에는 모바일만 돌리고, | ||
| # 배포 직전인 main 대상 PR에서만 PC 뷰포트를 함께 확인한다. | ||
| project: >- | ||
| ${{ github.base_ref == 'main' | ||
| && fromJSON('["mobile", "desktop"]') | ||
| || fromJSON('["mobile"]') }} | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| # 설정의 projects가 모두 Chromium 기반이라 브라우저 하나만 받으면 된다. | ||
| - name: Install Playwright browser | ||
| run: npx playwright install chromium --with-deps | ||
|
|
||
| # secrets 이름이 틀리면 값이 빈 문자열로 들어온다. 그러면 테스트는 | ||
| # 실패가 아니라 건너뛴 채 초록으로 끝나므로 그 전에 멈춘다. | ||
| - name: Check secrets | ||
| run: | | ||
| missing='' | ||
| for name in E2E_SUPABASE_URL E2E_SUPABASE_PUBLISHABLE_KEY E2E_SUPABASE_SECRET_KEY E2E_TEST_USER_PASSWORD; do | ||
| if [ -z "${!name}" ]; then missing="$missing $name"; fi | ||
| done | ||
| if [ -n "$missing" ]; then | ||
| echo "비어 있는 GitHub Secret:$missing" | ||
| exit 1 | ||
| fi | ||
| env: | ||
| E2E_SUPABASE_URL: ${{ secrets.E2E_SUPABASE_URL }} | ||
| E2E_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.E2E_SUPABASE_PUBLISHABLE_KEY }} | ||
| E2E_SUPABASE_SECRET_KEY: ${{ secrets.E2E_SUPABASE_SECRET_KEY }} | ||
| E2E_TEST_USER_PASSWORD: ${{ secrets.E2E_TEST_USER_PASSWORD }} | ||
|
|
||
| # secrets 이름에 E2E_를 붙여 운영 DB 값과 섞이지 않게 한다. | ||
| # 키 이름은 Supabase의 현재 명칭(publishable, secret)을 따른다. | ||
| # 앱 환경변수는 예전 명칭을 쓰고 있으므로 여기서 연결한다. | ||
| - name: Build | ||
| run: npm run build | ||
| env: | ||
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.E2E_SUPABASE_URL }} | ||
| NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.E2E_SUPABASE_PUBLISHABLE_KEY }} | ||
|
|
||
| - name: Run Playwright tests | ||
| run: npx playwright test --project=${{ matrix.project }} | ||
| env: | ||
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.E2E_SUPABASE_URL }} | ||
| NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.E2E_SUPABASE_PUBLISHABLE_KEY }} | ||
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.E2E_SUPABASE_SECRET_KEY }} | ||
| E2E_TEST_USER_PASSWORD: ${{ secrets.E2E_TEST_USER_PASSWORD }} | ||
|
|
||
| # 실패했을 때만 올린다. 리포트로 어떤 케이스가 깨졌는지 보고, | ||
| # trace는 `npx playwright show-trace trace.zip`으로 그 시점 화면과 네트워크를 확인한다. | ||
| - name: Upload report | ||
| if: failure() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-${{ matrix.project }}-${{ github.run_attempt }} | ||
| path: | | ||
| playwright-report/ | ||
| test-results/ | ||
| retention-days: 7 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # E2E 테스트 | ||
|
|
||
| Notion `테스트 시나리오` DB의 케이스를 Playwright 코드로 옮긴 것이다. | ||
| spec 파일은 도메인 단위, `test.describe()`는 시나리오 단위, `test()`는 테스트 케이스 하나에 대응한다. | ||
| 각 `test()` 위의 주석이 Notion `ID` 속성이다. | ||
|
|
||
| ## 실행 준비 | ||
|
|
||
| 로그인 이후 흐름을 검증하는 케이스는 Supabase 접속 정보가 필요하다. | ||
| 앱이 쓰는 값을 그대로 읽으므로 `.env.local`에 아래 값이 있으면 된다. | ||
| 값이 하나라도 없으면 해당 케이스는 실행되지 않고 건너뛴다. | ||
|
|
||
| ``` | ||
| NEXT_PUBLIC_SUPABASE_URL | ||
| NEXT_PUBLIC_SUPABASE_KEY | ||
| SUPABASE_SERVICE_ROLE_KEY | ||
| E2E_TEST_USER_PASSWORD | ||
| ``` | ||
|
|
||
| 앞의 둘은 브라우저가 쓰는 값이고, service role key는 seed와 cleanup을 위해 | ||
| Node.js helper에서만 쓴다. 브라우저 코드로 넘기지 않는다. | ||
| `E2E_TEST_USER_PASSWORD`는 테스트가 직접 만들고 지우는 계정에만 쓰는 비밀번호다. | ||
| 실제 사용자 계정과 무관하지만 저장소에 값을 두지 않으려고 환경변수로 분리했다. | ||
|
|
||
| **이 설정이 가리키는 프로젝트에 테스트 사용자와 그룹이 생성되고 삭제된다.** | ||
| 운영과 분리된 개발 프로젝트인지 확인하고 실행한다. | ||
| GitHub Actions에서는 GitHub Secrets로 같은 값을 주입한다. | ||
| 운영 DB 값과 섞이지 않도록 secrets 이름에는 `E2E_`를 붙이고 | ||
| 워크플로의 `env`에서 위 이름으로 연결한다. | ||
| 키 이름은 Supabase의 현재 명칭인 publishable key와 secret key를 쓴다. | ||
| 앱 환경변수는 이전 명칭(anon, service role)을 그대로 두었다. | ||
|
|
||
| | GitHub Secret | 연결되는 환경변수 | | ||
| | ------------------------------ | --------------------------- | | ||
| | `E2E_SUPABASE_URL` | `NEXT_PUBLIC_SUPABASE_URL` | | ||
| | `E2E_SUPABASE_PUBLISHABLE_KEY` | `NEXT_PUBLIC_SUPABASE_KEY` | | ||
| | `E2E_SUPABASE_SECRET_KEY` | `SUPABASE_SERVICE_ROLE_KEY` | | ||
| | `E2E_TEST_USER_PASSWORD` | `E2E_TEST_USER_PASSWORD` | | ||
|
|
||
| ## 실행 | ||
|
|
||
| ```bash | ||
| npm run test:e2e # 전체 (mobile + desktop) | ||
| npx playwright test --project=mobile # 모바일 뷰포트만 | ||
| npx playwright test e2e/settlement.spec.ts | ||
| npx playwright test --grep '납부 상태' | ||
| ``` | ||
|
|
||
| 뷰포트는 `mobile`(Pixel 5)과 `desktop`(Desktop Chrome) 두 프로젝트로 나눠 둔다. | ||
| 서비스가 모바일 우선이라 `mobile`을 기본으로 보고, 둘 다 Chromium 기반이라 | ||
| 브라우저 바이너리는 하나만 설치하면 된다. | ||
|
|
||
| ## CI | ||
|
|
||
| `.github/workflows/ci-e2e-test.yml`이 `develop`과 `main` 대상 PR에서 돌아간다. | ||
| `develop` 대상은 `mobile`만, `main` 대상은 `mobile`과 `desktop`을 함께 확인한다. | ||
|
|
||
| CI에서는 `npm run dev` 대신 빌드 결과(`npm run start`)를 띄운다. | ||
| dev 서버는 경로마다 첫 진입에서 컴파일해 느리고 결과가 흔들리는데, | ||
| 빌드된 서버는 그렇지 않아 같은 16건이 3분에서 30초 수준으로 줄어든다. | ||
| 같은 이유로 CI에서만 워커를 2로 올린다. | ||
|
|
||
| 실패하면 `playwright-report/`와 `test-results/`가 실행 요약 페이지의 Artifacts에 올라간다. | ||
| trace는 내려받아 `npx playwright show-trace trace.zip`으로 확인한다. | ||
|
|
||
| ## 구조 | ||
|
|
||
| | 파일 | 역할 | | ||
| | --------------------- | ------------------------------------------------- | | ||
| | `fixtures/env.ts` | `.env` 로딩, 테스트 DB 접속 정보와 실행 조건 판정 | | ||
| | `fixtures/seed.ts` | 사용자·그룹·참여자·초대·정산 기록 생성과 cleanup | | ||
| | `fixtures/session.ts` | Supabase 세션 생성과 브라우저 주입 | | ||
| | `fixtures/ui.ts` | 라벨·참여자 카드·토스트 등 공용 선택자 | | ||
| | `fixtures/test.ts` | `seed` fixture를 붙인 `test` | | ||
|
|
||
| ## 인증 방식 | ||
|
|
||
| 소셜 로그인 화면은 Google과 Kakao가 그리는 화면이라 자동화 대상이 아니다. | ||
| 테스트는 Supabase에 만든 테스트 계정으로 세션을 발급받아 브라우저 `localStorage`에 넣고 시작한다. | ||
| 세션 직렬화는 supabase 클라이언트가 직접 하도록 두고 테스트는 그 결과만 옮기므로, | ||
| 라이브러리의 저장 형식이 바뀌어도 테스트가 따라 깨지지 않는다. | ||
|
|
||
| `ProtectRoute`는 `localStorage`의 `user-store`로 로그인 여부를 판단하므로 세션과 함께 주입한다. | ||
| 앱이 이 값을 직접 채우는 흐름(로그인 콜백)을 검증할 때만 `withUserStore: false`로 둔다. | ||
|
|
||
| ## 데이터 정리 | ||
|
|
||
| `seed` fixture는 테스트가 실패해도 만든 데이터를 되돌린다. | ||
| 화면 조작으로 생기는 그룹처럼 아이디를 미리 알 수 없는 데이터는 | ||
| `seed.trackNbreadTitle(title)`로 제목을 예약해 두면 cleanup이 찾아서 지운다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { expect, test } from './fixtures/test' | ||
| import { hasTestDatabase, testDatabaseSkipReason } from './fixtures/env' | ||
| import { applySession, createSession, readUserStore } from './fixtures/session' | ||
|
|
||
| test.describe('접근 제어', () => { | ||
| // AUTH-ACCESS-001 | ||
| test('비로그인 사용자가 보호 경로를 열면 첫 화면으로 이동한다', async ({ | ||
| page, | ||
| }) => { | ||
| await page.goto('/home') | ||
|
|
||
| await expect(page).toHaveURL('/') | ||
| await expect( | ||
| page.getByRole('heading', { name: '이번 달 엔빵' }), | ||
| ).not.toBeVisible() | ||
| }) | ||
| }) | ||
|
|
||
| test.describe('로그인 콜백 처리', () => { | ||
| test.skip(!hasTestDatabase, testDatabaseSkipReason) | ||
|
|
||
| // AUTH-CALLBACK-001 | ||
| test('약관에 동의한 사용자는 인증 콜백 뒤 요청한 초대 경로로 이동한다', async ({ | ||
| page, | ||
| seed, | ||
| }) => { | ||
| const user = await seed.createUser() | ||
| const session = await createSession(user) | ||
|
|
||
| // user-store는 콜백 흐름이 직접 채워야 하는 값이므로 주입하지 않는다. | ||
| await applySession(page, session, { withUserStore: false }) | ||
|
|
||
| await page.goto('/auth/callback?next=%2Finvite%2Fsample-code') | ||
|
|
||
| await expect(page).toHaveURL('/invite/sample-code') | ||
|
|
||
| const userStore = await readUserStore(page) | ||
| expect(userStore?.state?.user?.id).toBe(user.id) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { loadEnvConfig } from '@next/env' | ||
|
|
||
| // Playwright는 Next.js와 달리 .env 파일을 자동으로 읽지 않으므로 같은 방식으로 직접 불러온다. | ||
| loadEnvConfig(process.cwd(), true, { | ||
| info: () => {}, | ||
| error: () => {}, | ||
| }) | ||
|
|
||
| /** | ||
| * 앱이 쓰는 Supabase 설정을 그대로 사용한다. 운영과 분리된 개발 프로젝트를 가리킨다. | ||
| * service role key는 seed와 cleanup에만 쓰고 브라우저 코드로 넘기지 않는다. | ||
| */ | ||
| export const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? '' | ||
| export const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_KEY ?? '' | ||
| export const supabaseServiceRoleKey = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? | ||
| process.env.SUPABASE_SECRET_KEY ?? | ||
| '') as string | ||
|
|
||
| /** 테스트가 만들고 지우는 계정에만 쓰는 비밀번호. 저장소에 값을 두지 않는다. */ | ||
| export const testUserPassword = process.env.E2E_TEST_USER_PASSWORD ?? '' | ||
|
|
||
| export const hasTestDatabase = Boolean( | ||
| supabaseUrl && supabaseAnonKey && supabaseServiceRoleKey && testUserPassword, | ||
| ) | ||
|
|
||
| export const testDatabaseSkipReason = | ||
| 'NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_KEY, SUPABASE_SERVICE_ROLE_KEY, E2E_TEST_USER_PASSWORD가 없어 데이터가 필요한 케이스를 건너뜁니다.' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For same-repository pull requests, this job checks out the proposed merge commit and then runs its Playwright configuration, tests, and Next.js server with the Supabase service-role secret in the environment. Anyone permitted to open a branch PR can therefore add arbitrary Node or server code that transmits this key before the change is reviewed; because the key bypasses RLS, disclosure grants unrestricted access to the test project. Gate this privileged step behind a trusted environment approval or execute only trusted test code rather than passing the secret directly to candidate code.
AGENTS.md reference: AGENTS.md:L105-L109
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
유효한 지적입니다. 메커니즘을 확인했습니다.
pull_request이벤트가 제안된 병합 커밋의 코드를 실행하고, 그 코드가SUPABASE_SERVICE_ROLE_KEY가 든 환경에서 돌아가는 것이 맞습니다. fork PR을 배제한 설정은 이 경로를 막지 못합니다. 공격자가 fork가 아니라 같은 저장소의 브랜치를 쓰기 때문입니다.이번 PR에서는 수용하고 #212로 분리했습니다.
노출되는 값은 개발 Supabase의 secret key 하나이고, 해당 프로젝트에는 실제 고객 데이터가 없습니다(
user행 4개). 악용 가능한 사람도 조직에 push 권한이 있는 인원으로 한정됩니다. 운영에는 닿지 않습니다.제안하신 environment 승인 게이트는 검토했으나 선택하지 않았습니다. 노출은 막지만 매 PR마다 수동 승인이 생겨, 자동 게이트를 도입하는 이 PR의 목적과 정면으로 부딪힙니다.
구조를 바꾸는 것은 권한 자체를 좁히는 방식이라고 판단해 #212에서 다룹니다. seed와 cleanup을 서버 쪽 진입점 뒤로 옮기고, CI에는 E2E 테스트 계정과 그 데이터만 다룰 수 있는 자격증명만 두는 방향입니다.
다만 이 판단은 "개발 DB에 지킬 데이터가 없다"는 전제에 기대고 있어, 전제가 바뀌면 #212의 우선순위를 올려야 합니다. 수용 근거는 PR 본문에도 남겼습니다.