diff --git a/__tests__/components/Navbar.test.tsx b/__tests__/components/Navbar.test.tsx index 0dc9e0b..ac626e3 100644 --- a/__tests__/components/Navbar.test.tsx +++ b/__tests__/components/Navbar.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Navbar from '@/components/Navbar/'; -import { render } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; describe('Navbar', () => { @@ -12,16 +12,40 @@ describe('Navbar', () => { it('should have dropdown menu', () => { const { container } = render(); - expect(container.querySelector('ul')).toHaveTextContent('Profile'); - expect(container.querySelector('ul')).toHaveTextContent('Dashboard'); - expect(container.querySelector('ul')).toHaveTextContent('Settings'); + expect(container.querySelector('ul')).toBeInTheDocument(); + expect(container.querySelector('ul')).toContainHTML('Profile'); + expect(container.querySelector('ul')).toContainHTML('Dashboard'); + expect(container.querySelector('ul')).toContainHTML('Settings'); + expect(container.querySelector('ul')).toContainHTML('Sign Out'); }); - it('should toggle menu', () => { - const { container } = render(); - const button = container.querySelector('button'); - expect(button).toHaveTextContent('Sunny'); - button?.click(); - expect(container.querySelector('ul')).toHaveClass('lg:flex space-x-4'); + it('should have google login button', () => { + render(); + const googleLoginButton = screen.getByTestId('google-login'); + expect(googleLoginButton).toBeInTheDocument(); + expect(googleLoginButton).toHaveTextContent('Sign In'); + expect(googleLoginButton).toHaveAttribute('href', 'https://api-tinysite.onrender.com/v1/auth/google/login'); + }); + + it('should display "Sign In" when not logged in', () => { + render(); + const signInButton = screen.getByText('Sign In'); + expect(signInButton).toBeInTheDocument(); + }); + + it('should handle "Sign Out" button click', () => { + render(); + const signOutButton = screen.getByText('Sign Out'); + expect(signOutButton).toBeInTheDocument(); + + const originalIsLoggedIn = screen.getByText('Sign In'); + fireEvent.click(originalIsLoggedIn); + + expect(signOutButton).toBeInTheDocument(); + + fireEvent.click(signOutButton); + + const signInButton = screen.getByText('Sign In'); + expect(signInButton).toBeInTheDocument(); }); }); diff --git a/__tests__/hooks/isAuthenticated.test.tsx b/__tests__/hooks/isAuthenticated.test.tsx new file mode 100644 index 0000000..c2e7a6c --- /dev/null +++ b/__tests__/hooks/isAuthenticated.test.tsx @@ -0,0 +1,28 @@ +import { renderHook } from '@testing-library/react-hooks'; +import fetchMock from 'jest-fetch-mock'; +import IsAuthenticated from '@/hooks/isAuthenticated'; +import { userData } from '../../fixtures/users'; + +beforeAll(() => { + fetchMock.enableMocks(); +}); + +afterEach(() => { + fetchMock.resetMocks(); +}); + +it('should return isLoggedIn as true and userData if the request is successful', async () => { + const userDataMock = userData; + fetchMock.mockResponseOnce(JSON.stringify(userDataMock), { status: 200 }); + const { result, waitForNextUpdate } = renderHook(() => IsAuthenticated()); + await waitForNextUpdate(); + expect(result.current.isLoggedIn).toBe(true); + expect(result.current.userData).toEqual(userDataMock.data); +}); + +it('should return isLoggedIn as false if the request is unsuccessful', async () => { + fetchMock.mockResponseOnce(JSON.stringify({}), { status: 401 }); + const { result } = renderHook(() => IsAuthenticated()); + expect(result.current.isLoggedIn).toBe(false); + expect(result.current.userData).toBeNull(); +}); diff --git a/fixtures/users.ts b/fixtures/users.ts new file mode 100644 index 0000000..cb4f072 --- /dev/null +++ b/fixtures/users.ts @@ -0,0 +1,13 @@ +export const userData = { + data: { + Id: 1, + Username: 'Sunny Sahsi', + Email: 'sunnysahsi@gmail.com', + Password: '', + IsVerified: false, + IsOnboarding: true, + CreatedAt: '2023-11-04T10:02:26.582699Z', + UpdatedAt: '2023-11-04T10:02:26.582699Z', + }, + message: 'user fetched successfully', +}; diff --git a/package.json b/package.json index 3ecb9f8..d28451a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "check:all": "yarn format:check && yarn lint:check", "fix:all": "yarn format:fix && yarn lint:fix", "prepare": "husky install", - "test": "jest --coverage", + "test": "jest --coverage -u", "lint": "eslint .", "lint-fix": "eslint . --fix" }, @@ -30,6 +30,7 @@ "devDependencies": { "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "^14.0.0", + "@testing-library/react-hooks": "^8.0.1", "@types/eslint": "^8", "@types/jest": "^29.5.3", "@types/node": "20.2.5", @@ -48,6 +49,7 @@ "husky": "^8.0.3", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", + "jest-fetch-mock": "^3.0.3", "prettier": "^2.8.8", "ts-jest": "^29.1.1" }, diff --git a/public/assets/icons/downArrow.tsx b/public/assets/icons/downArrow.tsx new file mode 100644 index 0000000..5ef5f0a --- /dev/null +++ b/public/assets/icons/downArrow.tsx @@ -0,0 +1,13 @@ +const DownArrow = () => ( + + + +); + +export default DownArrow; diff --git a/public/assets/icons/google.tsx b/public/assets/icons/google.tsx new file mode 100644 index 0000000..cc504ce --- /dev/null +++ b/public/assets/icons/google.tsx @@ -0,0 +1,23 @@ +const GoogleIcon = () => ( + + Google + + + + + +); + +export default GoogleIcon; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index d0ca95a..3d45f71 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,14 +1,6 @@ +import { ButtonProps } from '@/types/button.types'; import React from 'react'; -interface ButtonProps { - type?: 'button' | 'submit' | 'reset'; - className: string; - onClick: (event: React.MouseEvent) => void; - children: React.ReactNode; - disabled?: boolean; - testId?: string; -} - const Button: React.FC = ({ type, className, onClick, children, disabled, testId }) => { return ( + ) : ( + + - {firstName} - - - - - + Sign In + + )}
  • @@ -59,6 +72,11 @@ const Navbar: React.FC = () => { Settings
  • +
  • + + Sign Out + +
diff --git a/src/components/ProfileIcon/ProfileIcon.tsx b/src/components/ProfileIcon/ProfileIcon.tsx index e06f9b4..22ea737 100644 --- a/src/components/ProfileIcon/ProfileIcon.tsx +++ b/src/components/ProfileIcon/ProfileIcon.tsx @@ -1,11 +1,6 @@ +import { ProfileIconProps } from '@/types/profileIcon.types'; import React from 'react'; -interface ProfileIconProps { - firstName: string; - lastName: string; - size: number; -} - const ProfileIcon: React.FC = ({ firstName, lastName, size }) => { const initials = (firstName[0] + lastName[0]).toUpperCase(); const randomColor = Math.floor(Math.random() * 16777215).toString(16); diff --git a/src/constants/url.ts b/src/constants/url.ts new file mode 100644 index 0000000..2bc6f6d --- /dev/null +++ b/src/constants/url.ts @@ -0,0 +1,3 @@ +export const TINY_API_URL = 'https://api-tinysite.onrender.com/v1'; +export const TINY_API_GOOGLE_LOGIN = `${TINY_API_URL}/auth/google/login`; +export const TINY_API_LOGOUT = `${TINY_API_URL}/auth/logout`; diff --git a/src/hooks/isAuthenticated.ts b/src/hooks/isAuthenticated.ts new file mode 100644 index 0000000..3e8a42e --- /dev/null +++ b/src/hooks/isAuthenticated.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; +import { TINY_API_URL } from '@/constants/url'; +import { UserTypes } from '@/types/user.types'; + +const IsAuthenticated = () => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [userData, setUserData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch(`${TINY_API_URL}/users/self`, { + method: 'GET', + credentials: 'include', + }); + if (response.status === 200) { + const userData = await response.json(); + setUserData(userData.data); + setIsLoggedIn(true); + } else { + setIsLoggedIn(false); + } + } catch (err) { + setIsLoggedIn(false); + console.error(err); + } + }; + + fetchData(); + }, []); + + return { isLoggedIn, userData }; +}; + +export default IsAuthenticated; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 58dac9d..45d3cca 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react'; import Button from '@/components/Button'; import InputBox from '@/components/InputBox'; import Layout from '@/components/Layout'; +import { randomString } from '@/utils/constants'; +import { useState } from 'react'; import AddIcon from '../../../public/assets/icons/add'; import CopyIcon from '../../../public/assets/icons/copy'; import ReloadIcon from '../../../public/assets/icons/reload'; @@ -10,12 +11,7 @@ const Dashboard = () => { const [url, getUrl] = useState(''); const [shortUrl, setUrl] = useState(''); - function generateRandomString() { - const randomString = Math.random().toString(36).substring(2, 7); - return randomString; - } const handleUniqueUrl = () => { - const randomString = generateRandomString(); setUrl(`https://rds.li/${randomString}`); }; return ( diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index baa6ef4..65ff85e 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button'; import InputBox from '@/components/InputBox'; import Layout from '@/components/Layout'; +import { alphanumicUnderscore } from '@/utils/constants'; import { ChangeEvent, SetStateAction, useState } from 'react'; const LoginPage = () => { @@ -12,7 +13,7 @@ const LoginPage = () => { const handleChange = (event: ChangeEvent) => { const inputValue = (event.target as HTMLInputElement).value; - const alphanumicUnderscore = /^[a-zA-Z0-9_]+$/; + if (event.target.name === 'username') { if (alphanumicUnderscore.test(inputValue)) { setUsernameBorder(' border-2 border-green-500'); diff --git a/src/types/button.types.ts b/src/types/button.types.ts new file mode 100644 index 0000000..7fdf1c2 --- /dev/null +++ b/src/types/button.types.ts @@ -0,0 +1,8 @@ +export interface ButtonProps { + type?: 'button' | 'submit' | 'reset'; + className: string; + onClick: (event: React.MouseEvent) => void; + children: React.ReactNode; + disabled?: boolean; + testId?: string; +} diff --git a/src/types/profileIcon.types.ts b/src/types/profileIcon.types.ts new file mode 100644 index 0000000..c02f82a --- /dev/null +++ b/src/types/profileIcon.types.ts @@ -0,0 +1,5 @@ +export interface ProfileIconProps { + firstName: string; + lastName: string; + size: number; +} diff --git a/src/types/user.types.ts b/src/types/user.types.ts new file mode 100644 index 0000000..6e5049e --- /dev/null +++ b/src/types/user.types.ts @@ -0,0 +1,10 @@ +export interface UserTypes { + id: number; + Username: string; + email: string; + password: string; + isVerified: boolean; + isOnboarding: boolean; + createdAt: string; + updatedAt: string; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..a80ce32 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,2 @@ +export const alphanumicUnderscore = /^[a-zA-Z0-9_]+$/; +export const randomString = Math.random().toString(36).substring(2, 7); diff --git a/yarn.lock b/yarn.lock index 4744ed2..ea5741a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -748,6 +748,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^14.0.0": version "14.0.0" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" @@ -1593,6 +1601,13 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" +cross-fetch@^3.0.4: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3086,6 +3101,14 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" @@ -3659,6 +3682,13 @@ next@13.5.0: "@next/swc-win32-ia32-msvc" "13.5.0" "@next/swc-win32-x64-msvc" "13.5.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4006,6 +4036,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +promise-polyfill@^8.1.3: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" + integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4056,6 +4091,13 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4585,6 +4627,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" @@ -4779,6 +4826,11 @@ watchpack@2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -4804,6 +4856,14 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"