diff --git a/.github/workflows/test-ci.yaml b/.github/workflows/test-ci.yaml new file mode 100644 index 0000000..08c7bfc --- /dev/null +++ b/.github/workflows/test-ci.yaml @@ -0,0 +1,60 @@ +name: Test CI + +on: + workflow_dispatch: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 21, 22, 23] + fail-fast: false # 한 버전 실패 시 전체 중단 방지 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "store-path=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.store-path }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run lint + run: pnpm run lint + + - name: Run tests + run: pnpm run test + + - name: Run build + run: pnpm run build \ No newline at end of file diff --git a/README.md b/README.md index 30fd4e3..f0547d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Velog Dashboard Project -- 백엔드 API 서버 +- Velog dashboard V2 백엔드, API 서버 +- ***`node 20+`*** ## Project Setup Guide diff --git a/src/__test__/sum.test.ts b/src/__test__/sum.test.ts deleted file mode 100644 index 853787b..0000000 --- a/src/__test__/sum.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -test('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); -}); diff --git a/src/repositories/__test__/post.repository.test.ts b/src/repositories/__test__/post.repository.test.ts new file mode 100644 index 0000000..3ded9ec --- /dev/null +++ b/src/repositories/__test__/post.repository.test.ts @@ -0,0 +1,145 @@ +import { Pool, QueryResult } from 'pg'; +import { PostRepository } from '@/repositories/post.repository'; +import { DBError } from '@/exception'; + +jest.mock('pg'); + +const mockPool: { + query: jest.Mock>>, unknown[]>; +} = { + query: jest.fn(), +}; + +describe('PostRepository', () => { + let repo: PostRepository; + + beforeEach(() => { + repo = new PostRepository(mockPool as unknown as Pool); + }); + + describe('findPostsByUserId', () => { + it('사용자의 게시글과 nextCursor를 반환해야 한다', async () => { + const mockPosts = [ + { id: 1, post_released_at: '2025-03-01T00:00:00Z', daily_view_count: 10, daily_like_count: 5 }, + { id: 2, post_released_at: '2025-03-02T00:00:00Z', daily_view_count: 20, daily_like_count: 15 }, + ]; + + mockPool.query.mockResolvedValue({ + rows: mockPosts, + rowCount: mockPosts.length, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const result = await repo.findPostsByUserId(1, undefined, 'released_at', false); + + expect(result.posts).toEqual(mockPosts); + expect(result).toHaveProperty('nextCursor'); + }); + + it('정렬 순서를 보장해야 한다', async () => { + const mockPosts = [ + { id: 2, post_released_at: '2025-03-02T00:00:00Z', daily_view_count: 20, daily_like_count: 15 }, + { id: 1, post_released_at: '2025-03-01T00:00:00Z', daily_view_count: 10, daily_like_count: 5 }, + ]; + + mockPool.query.mockResolvedValue({ + rows: mockPosts, + rowCount: mockPosts.length, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const result = await repo.findPostsByUserId(1, undefined, 'released_at', false); + expect(result.posts).toEqual(mockPosts); + expect(result.posts[0].id).toBeGreaterThan(result.posts[1].id); + }); + }); + + describe('getTotalPostCounts', () => { + it('사용자의 총 게시글 수를 반환해야 한다', async () => { + mockPool.query.mockResolvedValue({ + rows: [{ count: '10' }], + rowCount: 1, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const count = await repo.getTotalPostCounts(1); + expect(count).toBe(10); + }); + }); + + describe('에러 발생 시 처리', () => { + it('쿼리 실행 중 에러가 발생하면 DBError를 던져야 한다', async () => { + mockPool.query.mockRejectedValue(new Error('DB connection failed')); + + await expect(repo.findPostsByUserId(1)).rejects.toThrow(DBError); + await expect(repo.getTotalPostCounts(1)).rejects.toThrow(DBError); + }); + }); + + describe('getYesterdayAndTodayViewLikeStats', () => { + it('어제와 오늘의 조회수 및 좋아요 수를 반환해야 한다', async () => { + const mockStats = { + daily_view_count: 20, + daily_like_count: 15, + yesterday_views: 10, + yesterday_likes: 8, + last_updated_date: '2025-03-08T00:00:00Z', + }; + + mockPool.query.mockResolvedValue({ + rows: [mockStats], + rowCount: 1, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const result = await repo.getYesterdayAndTodayViewLikeStats(1); + expect(result).toEqual(mockStats); + }); + }); + + describe('findPostByPostId', () => { + it('특정 post ID에 대한 통계를 반환해야 한다', async () => { + const mockStats = [ + { date: '2025-03-08T00:00:00Z', daily_view_count: 50, daily_like_count: 30 }, + ]; + + mockPool.query.mockResolvedValue({ + rows: mockStats, + rowCount: mockStats.length, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const result = await repo.findPostByPostId(1); + expect(result).toEqual(mockStats); + }); + }); + + describe('findPostByPostUUID', () => { + it('특정 post UUID에 대한 통계를 반환해야 한다', async () => { + const mockStats = [ + { date: '2025-03-08T00:00:00Z', daily_view_count: 50, daily_like_count: 30 }, + ]; + + mockPool.query.mockResolvedValue({ + rows: mockStats, + rowCount: mockStats.length, + command: '', + oid: 0, + fields: [], + } as QueryResult); + + const result = await repo.findPostByPostUUID('uuid-1234', '2025-03-01', '2025-03-08'); + expect(result).toEqual(mockStats); + }); + }); +}); diff --git a/src/repositories/post.repository.ts b/src/repositories/post.repository.ts index 1c6116f..78ac5bd 100644 --- a/src/repositories/post.repository.ts +++ b/src/repositories/post.repository.ts @@ -169,6 +169,7 @@ export class PostRepository { pds.daily_like_count FROM posts_postdailystatistics pds WHERE pds.post_id = $1 + ORDER BY pds.date ASC `; const values: (number | string)[] = [postId];