diff --git a/_tests_/utils/formatDate.test.ts b/_tests_/utils/formatDate.test.ts new file mode 100644 index 00000000..4c20d295 --- /dev/null +++ b/_tests_/utils/formatDate.test.ts @@ -0,0 +1,123 @@ +import { formatDate, formatISOtoDate } from '@/utils/formatDate'; + +describe('formatDate', () => { + it('기본 YYYY-MM-DD 형식을 YYYY.MM.DD로 변환해야 한다', () => { + const input = '2024-01-15'; + const result = formatDate(input); + expect(result).toBe('2024.01.15'); + }); + + it('다양한 날짜 형식을 올바르게 변환해야 한다', () => { + const testCases = [ + { input: '2023-12-31', expected: '2023.12.31' }, + { input: '2024-02-29', expected: '2024.02.29' }, + { input: '2025-06-15', expected: '2025.06.15' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = formatDate(input); + expect(result).toBe(expected); + }); + }); + + it('한 자리 월/일을 두 자리로 패딩해야 한다', () => { + const testCases = [ + { input: '2024-1-15', expected: '2024.01.15' }, + { input: '2024-01-1', expected: '2024.01.01' }, + { input: '2024-1-1', expected: '2024.01.01' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = formatDate(input); + expect(result).toBe(expected); + }); + }); + + it('빈 문자열이 입력되면 null을 반환해야 한다', () => { + const input = ''; + const result = formatDate(input); + expect(result).toBeNull(); + }); + + it('하이픈이 없는 문자열은 null을 반환해야 한다', () => { + const input = '20240115'; + const result = formatDate(input); + expect(result).toBeNull(); + }); + + it('공백이 포함된 날짜도 처리해야 한다', () => { + const input = ' 2024-01-15 '; + const result = formatDate(input); + expect(result).toBe('2024.01.15'); + }); +}); + +describe('formatISOtoDate', () => { + it('기본 YYYY-MM-DD HH:mm:ss 형식을 YYYY.MM.DD HH:mm로 변환해야 한다', () => { + const input = '2024-01-15 14:30:25'; + const result = formatISOtoDate(input); + expect(result).toBe('2024.01.15 14:30'); + }); + + it('다양한 시간 형식을 올바르게 변환해야 한다', () => { + const testCases = [ + { input: '2023-12-31 23:59:59', expected: '2023.12.31 23:59' }, + { input: '2024-06-15 09:30:00', expected: '2024.06.15 09:30' }, + { input: '2025-02-14 12:00:30', expected: '2025.02.14 12:00' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = formatISOtoDate(input); + expect(result).toBe(expected); + }); + }); + + it('한 자리 시간/분을 두 자리로 패딩해야 한다', () => { + const testCases = [ + { input: '2024-3-5 9:5:00', expected: '2024.03.05 09:05' }, + { input: '2024-12-1 15:30:05', expected: '2024.12.01 15:30' }, + { input: '2024-1-9 8:45:30', expected: '2024.01.09 08:45' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = formatISOtoDate(input); + expect(result).toBe(expected); + }); + }); + + it('초 부분이 제거되어야 한다', () => { + const testCases = [ + { input: '2024-01-15 14:30:45', expected: '2024.01.15 14:30' }, + { input: '2024-01-15 14:30:00', expected: '2024.01.15 14:30' }, + ]; + + testCases.forEach(({ input, expected }) => { + const result = formatISOtoDate(input); + expect(result).toBe(expected); + }); + }); + + it('빈 문자열이 입력되면 null을 반환해야 한다', () => { + const input = ''; + const result = formatISOtoDate(input); + expect(result).toBeNull(); + }); + + it('시간 부분이 없는 날짜도 처리해야 한다', () => { + const input = '2024-01-15'; + const result = formatISOtoDate(input); + expect(result).toBe('2024.01.15'); + }); + + it('시간만 있는 형식도 처리해야 한다', () => { + const input = '14:30:25'; + const result = formatISOtoDate(input); + expect(result).toBe('14:30'); + }); + + it('밀리초가 포함된 형식도 처리해야 한다', () => { + const input = '2024-01-15 14:30:25.123'; + const result = formatISOtoDate(input); + expect(result).toBe('2024.01.15 14:30'); + }); +}); diff --git a/_tests_/utils/getCookie.test.ts b/_tests_/utils/getCookie.test.ts new file mode 100644 index 00000000..cd3fff8e --- /dev/null +++ b/_tests_/utils/getCookie.test.ts @@ -0,0 +1,254 @@ +import { getCookie, checkLogin, getGA } from '@/utils/getCookie'; + +const setMockCookies = (cookies: string) => { + Object.defineProperty(document, 'cookie', { + writable: true, + value: cookies, + }); +}; + +describe('getCookie', () => { + beforeEach(() => { + setMockCookies(''); + }); + + it('존재하는 쿠키의 값을 반환해야 한다', () => { + setMockCookies('testCookie=testValue; otherCookie=otherValue'); + + const result = getCookie('testCookie'); + + expect(result).toBe('testValue'); + }); + + it('존재하지 않는 쿠키는 null을 반환해야 한다', () => { + setMockCookies('testCookie=testValue'); + + const result = getCookie('nonexistentCookie'); + + expect(result).toBeNull(); + }); + + it('URL 인코딩된 쿠키 값을 올바르게 디코딩해야 한다', () => { + setMockCookies('encodedCookie=test%40example.com'); + + const result = getCookie('encodedCookie'); + + expect(result).toBe('test@example.com'); + }); + + it('공백이 있는 쿠키도 올바르게 처리해야 한다', () => { + setMockCookies(' spacedCookie = spacedValue '); + + const result = getCookie('spacedCookie'); + + expect(result).toBe('spacedValue'); + }); + + it('빈 쿠키 문자열에서도 null을 반환해야 한다', () => { + setMockCookies(''); + + const result = getCookie('anyCookie'); + + expect(result).toBeNull(); + }); + + it('여러 쿠키 중에서 정확한 쿠키를 찾아야 한다', () => { + setMockCookies('cookie1=value1; cookie2=value2; cookie3=value3'); + + const result = getCookie('cookie2'); + + expect(result).toBe('value2'); + }); + + it('쿠키 값에 특수문자가 포함되어도 올바르게 처리해야 한다', () => { + setMockCookies('specialCookie=value%20with%20spaces%26symbols'); + + const result = getCookie('specialCookie'); + + expect(result).toBe('value with spaces&symbols'); + }); + + it('쿠키 값이 숫자여도 문자열로 반환해야 한다', () => { + setMockCookies('numberCookie=12345'); + + const result = getCookie('numberCookie'); + + expect(result).toBe('12345'); + }); + + it('쿠키 값이 JSON 형태여도 올바르게 처리해야 한다', () => { + setMockCookies('jsonCookie=%7B%22name%22%3A%22test%22%7D'); + + const result = getCookie('jsonCookie'); + + expect(result).toBe('{"name":"test"}'); + }); + + it('쿠키 이름에 대소문자를 구분해야 한다', () => { + setMockCookies('TestCookie=value; testcookie=otherValue'); + + const result = getCookie('TestCookie'); + + expect(result).toBe('value'); + }); + + it('빈 값의 쿠키도 올바르게 처리해야 한다', () => { + setMockCookies('emptyCookie=; otherCookie=value'); + + const result = getCookie('emptyCookie'); + + expect(result).toBe(''); + }); +}); + +describe('checkLogin', () => { + beforeEach(() => { + setMockCookies(''); + }); + + it('DEVDEVDEV_LOGIN_STATUS가 active이면 active를 반환해야 한다', () => { + setMockCookies('DEVDEVDEV_LOGIN_STATUS=active'); + + const result = checkLogin(); + + expect(result).toBe('active'); + }); + + it('DEVDEVDEV_LOGIN_STATUS가 active가 아니면 checking을 반환해야 한다', () => { + setMockCookies('DEVDEVDEV_LOGIN_STATUS=inactive'); + + const result = checkLogin(); + + expect(result).toBe('checking'); + }); + + it('DEVDEVDEV_LOGIN_STATUS 쿠키가 없으면 checking을 반환해야 한다', () => { + setMockCookies('otherCookie=value'); + + const result = checkLogin(); + + expect(result).toBe('checking'); + }); + + it('DEVDEVDEV_LOGIN_STATUS가 빈 값이면 checking을 반환해야 한다', () => { + setMockCookies('DEVDEVDEV_LOGIN_STATUS='); + + const result = checkLogin(); + + expect(result).toBe('checking'); + }); + + it('DEVDEVDEV_LOGIN_STATUS가 대소문자 구분 없이 active이면 active를 반환해야 한다', () => { + setMockCookies('DEVDEVDEV_LOGIN_STATUS=ACTIVE'); + + const result = checkLogin(); + + expect(result).toBe('active'); + }); + + it('다른 로그인 상태 값들도 checking을 반환해야 한다', () => { + const testCases = ['pending', 'failed', 'expired', 'unknown']; + + testCases.forEach((status) => { + setMockCookies(`DEVDEVDEV_LOGIN_STATUS=${status}`); + + const result = checkLogin(); + + expect(result).toBe('checking'); + }); + }); +}); + +describe('getGA', () => { + beforeEach(() => { + setMockCookies(''); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('_ga 쿠키가 있으면 해당 값을 반환해야 한다', async () => { + setMockCookies('_ga=GA1.2.123456789.1234567890'); + + const resultPromise = getGA(); + + const result = await resultPromise; + expect(result).toBe('GA1.2.123456789.1234567890'); + }); + + it('_ga 쿠키가 없으면 undefined를 반환해야 한다', async () => { + setMockCookies('otherCookie=value'); + + const resultPromise = getGA(); + + await jest.runAllTimersAsync(); + + const result = await resultPromise; + expect(result).toBeUndefined(); + }); + + it('최대 재시도 횟수만큼 시도해야 한다', async () => { + setMockCookies('otherCookie=value'); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + + const resultPromise = getGA(); + + await jest.runAllTimersAsync(); + + await resultPromise; + expect(consoleSpy).toHaveBeenCalledWith('Failed to get GA cookie after maximum retries'); + + consoleSpy.mockRestore(); + }); + + it('재시도 중에 쿠키가 생기면 즉시 반환해야 한다', async () => { + setMockCookies('otherCookie=value'); + + const resultPromise = getGA(); + + jest.advanceTimersByTime(1000); + setMockCookies('_ga=GA1.2.123456789.1234567890; otherCookie=value'); + + const result = await resultPromise; + expect(result).toBe('GA1.2.123456789.1234567890'); + }); + + it('다양한 GA 쿠키 형식을 올바르게 처리해야 한다', async () => { + const testCases = [ + 'GA1.1.123456789.1234567890', + 'GA1.2.987654321.0987654321', + 'GA1.3.111111111.2222222222', + ]; + + for (const gaCookie of testCases) { + setMockCookies(`_ga=${gaCookie}`); + + const resultPromise = getGA(); + + const result = await resultPromise; + expect(result).toBe(gaCookie); + } + }); + + it('GA 쿠키가 URL 인코딩되어 있어도 올바르게 처리해야 한다', async () => { + setMockCookies('_ga=GA1.2.123456789.1234567890%3B'); + + const resultPromise = getGA(); + + const result = await resultPromise; + expect(result).toBe('GA1.2.123456789.1234567890;'); + }); + + it('빈 GA 쿠키 값도 undefined로 처리해야 한다', async () => { + setMockCookies('_ga='); + + const resultPromise = getGA(); + + await jest.runAllTimersAsync(); + + const result = await resultPromise; + expect(result).toBeUndefined(); + }); +}); diff --git a/_tests_/utils/getUserInfo.test.ts b/_tests_/utils/getUserInfo.test.ts new file mode 100644 index 00000000..52c5c21e --- /dev/null +++ b/_tests_/utils/getUserInfo.test.ts @@ -0,0 +1,67 @@ +import { getMaskedEmail } from '@/utils/getUserInfo'; + +describe('getMaskedEmail', () => { + it('이메일의 @ 앞부분을 마스킹 처리해야 한다', () => { + const email = 'test@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('tes*'); + }); + + it('긴 이메일 주소도 올바르게 마스킹해야 한다', () => { + const email = 'verylongemail@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('ver**********'); + }); + + it('짧은 이메일 주소도 올바르게 처리해야 한다', () => { + const email = 'ab@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('ab'); + }); + + it('3글자 이메일 주소는 마스킹 없이 반환해야 한다', () => { + const email = 'abc@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('abc'); + }); + + it('4글자 이메일 주소는 1글자만 마스킹해야 한다', () => { + const email = 'abcd@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('abc*'); + }); + + it('다양한 도메인도 올바르게 처리해야 한다', () => { + const email = 'user@gmail.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('use*'); + }); + + it('특수문자가 포함된 이메일도 처리해야 한다', () => { + const email = 'user.name@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('use******'); + }); + + it('숫자가 포함된 이메일도 처리해야 한다', () => { + const email = 'user123@example.com'; + + const result = getMaskedEmail(email); + + expect(result).toBe('use****'); + }); +}); diff --git a/_tests_/utils/headerUtils.test.ts b/_tests_/utils/headerUtils.test.ts new file mode 100644 index 00000000..f3a7bee1 --- /dev/null +++ b/_tests_/utils/headerUtils.test.ts @@ -0,0 +1,76 @@ +import { isNavigationActive } from '@/utils/headerUtils'; + +describe('isNavigationActive', () => { + it('정확히 일치하는 경로에서 true를 반환해야 한다', () => { + const testCases: Array<{ link: '/pickpickpick' | '/techblog' | '/myinfo'; pathname: string }> = + [ + { link: '/pickpickpick', pathname: '/pickpickpick' }, + { link: '/techblog', pathname: '/techblog' }, + { link: '/myinfo', pathname: '/myinfo' }, + ]; + + testCases.forEach(({ link, pathname }) => { + const result = isNavigationActive(link, pathname); + expect(result).toBe(true); + }); + }); + + it('하위 경로에서도 true를 반환해야 한다', () => { + const testCases: Array<{ link: '/pickpickpick' | '/techblog' | '/myinfo'; pathname: string }> = + [ + { link: '/pickpickpick', pathname: '/pickpickpick/detail' }, + { link: '/techblog', pathname: '/techblog/post/123' }, + { link: '/myinfo', pathname: '/myinfo/settings' }, + ]; + + testCases.forEach(({ link, pathname }) => { + const result = isNavigationActive(link, pathname); + expect(result).toBe(true); + }); + }); + + it('다른 경로에서는 false를 반환해야 한다', () => { + const testCases: Array<{ link: '/pickpickpick' | '/techblog' | '/myinfo'; pathname: string }> = + [ + { link: '/pickpickpick', pathname: '/techblog' }, + { link: '/techblog', pathname: '/myinfo' }, + { link: '/myinfo', pathname: '/pickpickpick' }, + ]; + + testCases.forEach(({ link, pathname }) => { + const result = isNavigationActive(link, pathname); + expect(result).toBe(false); + }); + }); + + it('비슷한 경로명에서도 정확히 구분해야 한다', () => { + const testCases: Array<{ link: '/pickpickpick' | '/techblog' | '/myinfo'; pathname: string }> = + [ + { link: '/myinfo', pathname: '/myinfosettings' }, + { link: '/techblog', pathname: '/techblogpost' }, + { link: '/pickpickpick', pathname: '/pickpickpickdetail' }, + ]; + + testCases.forEach(({ link, pathname }) => { + const result = isNavigationActive(link, pathname); + expect(result).toBe(false); + }); + }); + + it('경계 케이스에서 올바르게 동작해야 한다', () => { + const testCases: Array<{ + link: '/pickpickpick' | '/techblog' | '/myinfo'; + pathname: string; + expected: boolean; + }> = [ + { link: '/pickpickpick', pathname: '', expected: false }, + { link: '/techblog', pathname: '/', expected: false }, + { link: '/myinfo', pathname: '/my', expected: false }, + ]; + + testCases.forEach(({ link, pathname, expected }) => { + const result = isNavigationActive(link, pathname); + expect(result).toBe(expected); + }); + }); +}); diff --git a/components/common/header/header.tsx b/components/common/header/header.tsx index 368fd607..d5af8340 100644 --- a/components/common/header/header.tsx +++ b/components/common/header/header.tsx @@ -4,7 +4,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { isActive } from '@utils/headerUtils'; +import { isNavigationActive } from '@utils/headerUtils'; import { useLoginStatusStore } from '@stores/loginStore'; import { useLoginModalStore } from '@stores/modalStore'; @@ -50,7 +50,7 @@ export default function Header() {