diff --git a/frontend/cypress/e2e/main-page.cy.ts b/frontend/cypress/e2e/main-page.cy.ts index 344d009df..7e48e1238 100644 --- a/frontend/cypress/e2e/main-page.cy.ts +++ b/frontend/cypress/e2e/main-page.cy.ts @@ -4,51 +4,27 @@ describe('동글 메인 페이지', () => { cy.visit(`/`); }); - describe('글 업로드 테스트', () => { - it('Add Post 버튼을 누르면 글 가져오기 모달 창이 열린다.', () => { - cy.findByText('Add Post').click(); + describe('로그인 성공 테스트', () => { + it('로그인 하기 버튼을 누르면 로그인 모달 창이 열린다.', () => { + cy.findByText('로그인하기').click(); - cy.findByText('글 가져오기').should('exist'); + cy.findByText('간편 로그인').should('exist'); }); - it('드래그 앤 드롭으로 마크다운 파일을 업로드할 수 있다.', () => { - cy.findByText('Add Post').click(); - cy.findByLabelText('파일 업로드').attachFile('markdown-test.md', { - subjectType: 'drag-n-drop', - }); - - cy.findByText('markdown-test').should('exist'); - cy.findByText('e2e 테스트를 위한 마크다운 파일입니다.').should('exist'); - cy.findByText('글 정보').should('exist'); - cy.findByLabelText('오른쪽 사이드바 토글').should('exist'); - }); - - it('기본 카테고리에서 업로드한 글을 확인할 수 있다.', () => { - cy.findByText('기본').click(); - - cy.findAllByText('markdown-test').should('exist'); + it('카카오 로그인 버튼을 누르면 카카오로 로그인 할 수 있는 화면이 나타난다.', () => { + cy.findByText('로그인하기').click(); + cy.findByLabelText('카카오 로그인 화면으로 이동').click(); + cy.visit(`/oauth/login/kakao?code=mock`); + cy.findByText('로그아웃').should('exist'); }); }); - describe('카테고리 테스트', () => { - it('카테고리 추가 버튼을 클릭하여 입력 창에 이름을 입력하고 엔터를 쳐서 카테고리를 추가할 수 있다.', () => { - cy.findByLabelText('카테고리 추가 입력 창 열기').click(); - cy.findByLabelText('카테고리 추가 입력 창').focus().type('동글이{enter}'); - cy.findByText('동글이').should('exist'); - }); - - it('카테고리 이름 수정 버튼을 클릭하여 입력 창에 이름을 입력하고 엔터를 쳐서 카테고리 이름을 수정할 수 있다.', () => { - cy.findByText('동글이').realHover(); - cy.findByLabelText('동글이 카테고리 이름 수정').click(); - cy.findByLabelText('동글이 카테고리 이름 수정 입력 창').focus().type('동글동글이{enter}'); - cy.findByText('동글이').should('not.exist'); - cy.findByText('동글동글이').should('exist'); - }); - - it('카테고리 삭제 버튼을 클릭하여 카테고리를 삭제할 수 있다.', () => { - cy.findByText('동글동글이').realHover(); - cy.findByLabelText('동글동글이 카테고리 삭제').click(); - cy.findByText('동글동글이').should('not.exist'); + describe('로그인 실패 테스트', () => { + it('카카오 로그인 버튼을 누르면 카카오로 로그인 할 수 있는 화면이 나타난다.', () => { + cy.findByText('로그인하기').click(); + cy.findByLabelText('카카오 로그인 화면으로 이동').click(); + cy.visit(`/oauth/login/kakao`); + cy.findByText('로그인을 실패했습니다').should('exist'); }); }); }); diff --git a/frontend/cypress/e2e/space-page.cy.ts b/frontend/cypress/e2e/space-page.cy.ts new file mode 100644 index 000000000..6b2060398 --- /dev/null +++ b/frontend/cypress/e2e/space-page.cy.ts @@ -0,0 +1,54 @@ +describe('동글 스페이스 페이지', () => { + beforeEach(() => { + cy.viewport(1440, 810); + cy.visit(`/`); + }); + + describe('글 업로드 테스트', () => { + it('Add Post 버튼을 누르면 글 가져오기 모달 창이 열린다.', () => { + cy.findByText('Add Post').click(); + + cy.findByText('글 가져오기').should('exist'); + }); + + it('드래그 앤 드롭으로 마크다운 파일을 업로드할 수 있다.', () => { + cy.findByText('Add Post').click(); + cy.findByLabelText('파일 업로드').attachFile('markdown-test.md', { + subjectType: 'drag-n-drop', + }); + + cy.findByText('markdown-test').should('exist'); + cy.findByText('e2e 테스트를 위한 마크다운 파일입니다.').should('exist'); + cy.findByText('글 정보').should('exist'); + cy.findByLabelText('오른쪽 사이드바 토글').should('exist'); + }); + + it('기본 카테고리에서 업로드한 글을 확인할 수 있다.', () => { + cy.findByText('기본').click(); + + cy.findAllByText('markdown-test').should('exist'); + }); + }); + + describe('카테고리 테스트', () => { + it('카테고리 추가 버튼을 클릭하여 입력 창에 이름을 입력하고 엔터를 쳐서 카테고리를 추가할 수 있다.', () => { + cy.findByLabelText('카테고리 추가 입력 창 열기').click(); + cy.findByLabelText('카테고리 추가 입력 창').focus().type('동글이{enter}'); + cy.findByText('동글이').should('exist'); + }); + + it('카테고리 이름 수정 버튼을 클릭하여 입력 창에 이름을 입력하고 엔터를 쳐서 카테고리 이름을 수정할 수 있다.', () => { + cy.findByText('동글이').realHover(); + cy.findByLabelText('동글이 카테고리 이름 수정').click(); + cy.findByLabelText('동글이 카테고리 이름 수정 입력 창').focus().type('동글동글이{enter}'); + cy.findByText('동글이').should('not.exist'); + cy.findByText('동글동글이').should('exist'); + }); + + it('카테고리 삭제 버튼을 클릭하여 카테고리를 삭제할 수 있다.', () => { + cy.findByText('동글동글이').realHover(); + cy.findByLabelText('동글동글이 카테고리 삭제').click(); + cy.findByText('동글동글이').should('not.exist'); + }); + }); +}); diff --git a/frontend/src/mocks/auth.ts b/frontend/src/mocks/auth.ts new file mode 100644 index 000000000..e9485a40f --- /dev/null +++ b/frontend/src/mocks/auth.ts @@ -0,0 +1,18 @@ +import { DefaultBodyType, PathParams, RestRequest } from 'msw'; + +export const MOCK_ACCESS_TOKEN = 'mockAccessToken'; + +export const ERROR_RESPONSE = { + error: { + message: '유효하지 않은 토큰입니다.', + hint: 'AccessToken이 만료되었습니다. RefreshToken값을 요청하세요.', + code: 4011, + }, +}; + +export const isValidAccessToken = (req: RestRequest>) => { + const authorizationHeader = req.headers.get('Authorization'); + + if (authorizationHeader === `Bearer ${MOCK_ACCESS_TOKEN}`) return true; + return false; +}; diff --git a/frontend/src/mocks/handlers/category.ts b/frontend/src/mocks/handlers/category.ts index 94d69cfe4..7ed91ed71 100644 --- a/frontend/src/mocks/handlers/category.ts +++ b/frontend/src/mocks/handlers/category.ts @@ -7,10 +7,13 @@ import { UpdateCategoryTitleArgs, } from 'types/apis/category'; import { hasDefinedField } from 'utils/typeGuard'; +import { ERROR_RESPONSE, isValidAccessToken } from 'mocks/auth'; export const categoryHandlers = [ // 카테고리 목록 조회 - rest.get(categoryURL, (_, res, ctx) => { + rest.get(categoryURL, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.json(categories), ctx.delay(300), ctx.status(200)); }), @@ -18,6 +21,8 @@ export const categoryHandlers = [ rest.post(categoryURL, (req, res, ctx) => { const body = req.body as AddCategoriesRequest; + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + if (!body || !body.categoryName) return res( ctx.delay(300), @@ -30,6 +35,8 @@ export const categoryHandlers = [ // 카테고리 글 목록 조회 rest.get(`${categoryURL}/:categoryId`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + const categoryId = Number(req.params.categoryId); if (categoryId !== 1 && categoryId !== 3) @@ -51,6 +58,8 @@ export const categoryHandlers = [ const categoryId = Number(req.params.categoryId); const body = await req.json(); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + // 카테고리 순서 변경 if (hasDefinedField(body, 'nextCategoryId')) { if (!body.nextCategoryId) { @@ -83,6 +92,8 @@ export const categoryHandlers = [ // 카테고리 이름 수정 if (hasDefinedField(body, 'categoryName')) { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + if (!body.categoryName) return res( ctx.delay(300), @@ -96,7 +107,9 @@ export const categoryHandlers = [ }), // 카테고리 삭제 - rest.delete(`${categoryURL}/:categoryId`, (_, res, ctx) => { + rest.delete(`${categoryURL}/:categoryId`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.delay(300), ctx.status(204)); }), ]; diff --git a/frontend/src/mocks/handlers/connections.ts b/frontend/src/mocks/handlers/connections.ts index 5b08e0c24..b6eae6b99 100644 --- a/frontend/src/mocks/handlers/connections.ts +++ b/frontend/src/mocks/handlers/connections.ts @@ -1,34 +1,47 @@ import { connectionsURL } from 'constants/apis/url'; +import { ERROR_RESPONSE, isValidAccessToken } from 'mocks/auth'; import { rest } from 'msw'; export const connectionsHandlers = [ // 티스토리 정보 저장 - rest.post(`${connectionsURL}/tistory`, (_, res, ctx) => { + rest.post(`${connectionsURL}/tistory`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 미디움 정보 저장 - rest.post(`${connectionsURL}/medium`, (_, res, ctx) => { + rest.post(`${connectionsURL}/medium`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 노션 정보 저장 - rest.post(`${connectionsURL}/notion`, (_, res, ctx) => { + rest.post(`${connectionsURL}/notion`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 티스토리 연결 해제 - rest.post(`${connectionsURL}/tistory/disconnect`, (_, res, ctx) => { + rest.post(`${connectionsURL}/tistory/disconnect`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 미디움 연결 해제 - rest.post(`${connectionsURL}/medium/disconnect`, (_, res, ctx) => { + rest.post(`${connectionsURL}/medium/disconnect`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 노션 연결 해제 - rest.post(`${connectionsURL}/notion/disconnect`, (_, res, ctx) => { + rest.post(`${connectionsURL}/notion/disconnect`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), ]; diff --git a/frontend/src/mocks/handlers/login.ts b/frontend/src/mocks/handlers/login.ts index 64749c2a8..22ace7c78 100644 --- a/frontend/src/mocks/handlers/login.ts +++ b/frontend/src/mocks/handlers/login.ts @@ -1,10 +1,11 @@ import { authURL, loginURL } from 'constants/apis/url'; +import { MOCK_ACCESS_TOKEN } from 'mocks/auth'; import { rest } from 'msw'; export const loginHandlers = [ // 카카오 로그인/회원가입: POST rest.post(`${loginURL}/kakao`, (_, res, ctx) => { - return res(ctx.delay(300), ctx.status(200)); + return res(ctx.delay(300), ctx.status(200), ctx.json({ accessToken: MOCK_ACCESS_TOKEN })); }), rest.post(`${authURL}/logout`, (_, res, ctx) => { diff --git a/frontend/src/mocks/handlers/member.ts b/frontend/src/mocks/handlers/member.ts index 3338a437f..9443c4c98 100644 --- a/frontend/src/mocks/handlers/member.ts +++ b/frontend/src/mocks/handlers/member.ts @@ -1,14 +1,20 @@ import { memberURL } from 'constants/apis/url'; +import { ERROR_RESPONSE, isValidAccessToken } from 'mocks/auth'; import { member } from 'mocks/memberContentsMock'; import { rest } from 'msw'; export const memberHandlers = [ - rest.get(memberURL, (_, res, ctx) => { + // 멤버 정보 가져오기: GET + rest.get(memberURL, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.json(member), ctx.status(200)); }), // 회원 탈퇴: POST - rest.post(`${memberURL}/delete`, (_, res, ctx) => { + rest.post(`${memberURL}/delete`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), ]; diff --git a/frontend/src/mocks/handlers/trash.ts b/frontend/src/mocks/handlers/trash.ts index cc774be62..95e44d3b1 100644 --- a/frontend/src/mocks/handlers/trash.ts +++ b/frontend/src/mocks/handlers/trash.ts @@ -1,20 +1,27 @@ import { trashURL } from 'constants/apis/url'; +import { ERROR_RESPONSE, isValidAccessToken } from 'mocks/auth'; import { deletedWritings } from 'mocks/trashCanContentsMock'; import { rest } from 'msw'; export const trashHandlers = [ // 글 휴지통으로 이동 / 글 영구 삭제 - rest.post(trashURL, (_, res, ctx) => { + rest.post(trashURL, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), // 휴지통에서 글 목록 조회 - rest.get(trashURL, (_, res, ctx) => { + rest.get(trashURL, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.json(deletedWritings), ctx.status(200)); }), // 휴지통에서 글 복구 - rest.post(`${trashURL}/restore`, (_, res, ctx) => { + rest.post(`${trashURL}/restore`, (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.status(200)); }), ]; diff --git a/frontend/src/mocks/handlers/writing.ts b/frontend/src/mocks/handlers/writing.ts index 2ee159316..110ee71a7 100644 --- a/frontend/src/mocks/handlers/writing.ts +++ b/frontend/src/mocks/handlers/writing.ts @@ -9,8 +9,8 @@ import { UpdateWritingOrderArgs, UpdateWritingTitleArgs, } from 'types/apis/writings'; -import { getWritingTableMock } from 'mocks/writingTableMock'; import { hasDefinedField } from 'utils/typeGuard'; +import { ERROR_RESPONSE, isValidAccessToken } from 'mocks/auth'; import { getHomeWritingMock } from 'mocks/homeWritingMock'; export const writingHandlers = [ @@ -21,7 +21,10 @@ export const writingHandlers = [ if (writingIdOrHome === 'home') return res(ctx.delay(300), ctx.status(200), ctx.json(getHomeWritingMock())); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + const writingId = Number(req.params.writingId); + if (writingId === 200) { return res( ctx.delay(300), @@ -40,6 +43,8 @@ export const writingHandlers = [ rest.get(`${writingURL}/:writingId/properties`, (req, res, ctx) => { const writingId = Number(req.params.writingId); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + if (writingId === 200) { return res( ctx.delay(300), @@ -71,13 +76,18 @@ export const writingHandlers = [ }), // 글 생성(글 업로드): POST - rest.post(`${writingURL}/file`, async (_, res, ctx) => { + rest.post(`${writingURL}/file`, async (req, res, ctx) => { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res(ctx.delay(3000), ctx.status(201), ctx.set('Location', `/writings/200`)); }), // 글 생성(글 업로드): POST - rest.post(`${writingURL}/notion`, async (_, res, ctx) => { + rest.post(`${writingURL}/notion`, async (req, res, ctx) => { // return res(ctx.delay(1000), ctx.status(201), ctx.set('Location', `/writings/200`)); + + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + return res( ctx.delay(1000), ctx.status(404), @@ -91,6 +101,8 @@ export const writingHandlers = [ const id = Number(req.params.writingId); const { publishTo } = await req.json(); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + if (!blog.includes(publishTo) || typeof id !== 'number') return res( ctx.status(404), @@ -106,16 +118,7 @@ export const writingHandlers = [ rest.get(`${writingURL}`, (req, res, ctx) => { const categoryId = Number(req.url.searchParams.get('categoryId')); - if (req.headers.get('Authorization') !== 'Bearer accessToken') - return res( - ctx.status(401), - ctx.json({ - error: { - message: '만료된 accessToken입니다. refreshToken을 이용해 갱신해주세요', - code: 4011, - }, - }), - ); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); return res( ctx.json(getWritingTableMock(categoryId)), @@ -129,6 +132,8 @@ export const writingHandlers = [ const writingId = Number(req.params.writingId); const body = await req.json(); + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + // 글 순서 수정 if (hasDefinedField(body, 'nextWritingId')) { if (!body.nextWritingId) { @@ -144,6 +149,8 @@ export const writingHandlers = [ // 글 이름 수정 if (hasDefinedField(body, 'title')) { + if (!isValidAccessToken(req)) return res(ctx.status(401), ctx.json(ERROR_RESPONSE)); + if (!body.title) return res( ctx.delay(300),