Add token valification unit test
This commit is contained in:
@@ -6,7 +6,7 @@ export type UserType = {
|
|||||||
password: string;
|
password: string;
|
||||||
username: string;
|
username: string;
|
||||||
role: number;
|
role: number;
|
||||||
avatarPath: string;
|
avatarPath: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type TokenProps = {
|
export type TokenProps = {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { createContext, useState, useEffect, useContext } from 'react';
|
|||||||
import { TokenContextType, TokenType } from '@/types/user';
|
import { TokenContextType, TokenType } from '@/types/user';
|
||||||
import { TokenProps } from '@/types/user';
|
import { TokenProps } from '@/types/user';
|
||||||
import { useRouter, usePathname } from '@/src/navigation';
|
import { useRouter, usePathname } from '@/src/navigation';
|
||||||
import { roles } from '@/config/selection';
|
import {
|
||||||
|
isSignedIn as tokenIsSinedIn,
|
||||||
|
isAdmin as tokenIsAdmin,
|
||||||
|
checkSignInPage as tokenCheckSignInPage,
|
||||||
|
} from './token';
|
||||||
import { ToastContext } from './ToastProvider';
|
import { ToastContext } from './ToastProvider';
|
||||||
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
||||||
|
|
||||||
@@ -40,39 +44,12 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
|||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenExists = () => {
|
|
||||||
if (token && token.user && token.user.username) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTokenValid = () => {
|
|
||||||
if (Date.now() < token.expires_at) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSignedIn = () => {
|
const isSignedIn = () => {
|
||||||
if (tokenExists() && isTokenValid()) {
|
return tokenIsSinedIn(token);
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdmin = () => {
|
const isAdmin = () => {
|
||||||
if (tokenExists() && isTokenValid()) {
|
return tokenIsAdmin(token);
|
||||||
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
|
|
||||||
if (token.user.role === adminRoleIndex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenContext = {
|
const tokenContext = {
|
||||||
@@ -98,32 +75,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// pravate paths are '/account', '/admin', '/projects/*'
|
tokenCheckSignInPage(token, pathname);
|
||||||
const isPrivatePath = (pathname: string) => {
|
|
||||||
return (
|
|
||||||
/^\/account(\/)?$/.test(pathname) || /^\/admin(\/)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkSignInPage = () => {
|
|
||||||
if (!hasRestoreFinished) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isPrivatePath(pathname) && !isSignedIn()) {
|
|
||||||
if (tokenExists()) {
|
|
||||||
toastContext.showToast(toastMessages.needSignedIn, 'error');
|
|
||||||
router.push(`/account/signin`, { locale: locale });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTokenValid()) {
|
|
||||||
toastContext.showToast(toastMessages.sessionExpired, 'error');
|
|
||||||
router.push(`/account/signin`, { locale: locale });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkSignInPage();
|
|
||||||
}, [pathname, hasRestoreFinished]);
|
}, [pathname, hasRestoreFinished]);
|
||||||
|
|
||||||
return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
||||||
|
|||||||
137
frontend/utils/token.test.ts
Normal file
137
frontend/utils/token.test.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { isPrivatePath, checkSignInPage, isAdmin } from './token';
|
||||||
|
|
||||||
|
describe('check account path', () => {
|
||||||
|
test('"account/" is private', () => {
|
||||||
|
expect(isPrivatePath('/account')).toBe(true);
|
||||||
|
});
|
||||||
|
test('"account/signin" is public', () => {
|
||||||
|
expect(isPrivatePath('/account/signin')).toBe(false);
|
||||||
|
});
|
||||||
|
test('"account/signup" is public', () => {
|
||||||
|
expect(isPrivatePath('/account/signup')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('check admin path', () => {
|
||||||
|
test('"admin/" is private', () => {
|
||||||
|
expect(isPrivatePath('/admin')).toBe(true);
|
||||||
|
});
|
||||||
|
test('"admin/something" is private', () => {
|
||||||
|
expect(isPrivatePath('/admin/signin')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('check projects path', () => {
|
||||||
|
test('"projects/" is private', () => {
|
||||||
|
expect(isPrivatePath('/projects')).toBe(true);
|
||||||
|
});
|
||||||
|
test('"projects/1" is private', () => {
|
||||||
|
expect(isPrivatePath('/projects/1')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const validAdminToken = {
|
||||||
|
access_token: 'loremipsumdolorsitametconsecteturadipiscingelit',
|
||||||
|
expires_at: Infinity,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'hashedpassword',
|
||||||
|
username: 'admin',
|
||||||
|
role: 0,
|
||||||
|
avatarPath: null,
|
||||||
|
createdAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
updatedAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const validUserToken = {
|
||||||
|
access_token: 'loremipsumdolorsitametconsecteturadipiscingelit',
|
||||||
|
expires_at: Infinity,
|
||||||
|
user: {
|
||||||
|
id: 2,
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'hashedpassword',
|
||||||
|
username: 'user',
|
||||||
|
role: 1,
|
||||||
|
avatarPath: null,
|
||||||
|
createdAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
updatedAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expiredUserToken = {
|
||||||
|
access_token: 'loremipsumdolorsitametconsecteturadipiscingelit',
|
||||||
|
expires_at: 1717200000000,
|
||||||
|
user: {
|
||||||
|
id: 2,
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'hashedpassword',
|
||||||
|
username: 'user',
|
||||||
|
role: 1,
|
||||||
|
avatarPath: null,
|
||||||
|
createdAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
updatedAt: '2024-06-01T00:18:03.859Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('check user is admin or not', () => {
|
||||||
|
test('user with admin role is admin', () => {
|
||||||
|
expect(isAdmin(validAdminToken)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user with user role is not admin', () => {
|
||||||
|
expect(isAdmin(validUserToken)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('check sign in', () => {
|
||||||
|
test('Access to public paths by not signed user will be allowed', () => {
|
||||||
|
const token = {
|
||||||
|
access_token: '',
|
||||||
|
expires_at: 0,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const privatePath = '/';
|
||||||
|
expect(checkSignInPage(token, privatePath)).toStrictEqual({
|
||||||
|
ok: true,
|
||||||
|
reason: '',
|
||||||
|
redirectPath: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access to private paths by not signed user in will be redirected', () => {
|
||||||
|
const token = {
|
||||||
|
access_token: '',
|
||||||
|
expires_at: 0,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const privatePath = '/account';
|
||||||
|
expect(checkSignInPage(token, privatePath)).toStrictEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: 'notoken',
|
||||||
|
redirectPath: '/account/signin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access to private paths by signed in user will be allowed', () => {
|
||||||
|
const privatePath = '/account';
|
||||||
|
expect(checkSignInPage(validAdminToken, privatePath)).toStrictEqual({
|
||||||
|
ok: true,
|
||||||
|
reason: '',
|
||||||
|
redirectPath: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access to private paths by user whose token expired will be redirected', () => {
|
||||||
|
const privatePath = '/account';
|
||||||
|
expect(checkSignInPage(expiredUserToken, privatePath)).toStrictEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: 'expired',
|
||||||
|
redirectPath: '/account/signin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
69
frontend/utils/token.ts
Normal file
69
frontend/utils/token.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { TokenType } from '@/types/user';
|
||||||
|
import { roles } from '@/config/selection';
|
||||||
|
|
||||||
|
function tokenExists(token: TokenType) {
|
||||||
|
if (token && token.user && token.user.username) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTokenValid(token: TokenType) {
|
||||||
|
if (Date.now() < token.expires_at) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSignedIn(token: TokenType): boolean {
|
||||||
|
if (tokenExists(token) && isTokenValid(token)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdmin(token: TokenType) {
|
||||||
|
if (tokenExists(token) && isTokenValid(token)) {
|
||||||
|
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
|
||||||
|
if (token.user.role === adminRoleIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pravate paths are '/account', '/admin', '/projects/*'
|
||||||
|
const isPrivatePath = (pathname: string) => {
|
||||||
|
return /^\/account(\/)?$/.test(pathname) || /^\/admin(\/.*)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkSignInPage(token: TokenType, pathname: string) {
|
||||||
|
let ret = {
|
||||||
|
ok: true,
|
||||||
|
reason: '',
|
||||||
|
redirectPath: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPrivatePath(pathname) && !isSignedIn(token)) {
|
||||||
|
if (!tokenExists(token)) {
|
||||||
|
ret.ok = false;
|
||||||
|
ret.reason = 'notoken';
|
||||||
|
ret.redirectPath = '/account/signin';
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
if (!isTokenValid(token)) {
|
||||||
|
ret.ok = false;
|
||||||
|
ret.reason = 'expired';
|
||||||
|
ret.redirectPath = '/account/signin';
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isSignedIn, isAdmin, isPrivatePath, checkSignInPage };
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ['**/node_modules/**', 'docs/**', '**/.next/**'],
|
exclude: ['**/node_modules/**', 'docs/**', '**/.next/**'],
|
||||||
provider: "v8",
|
provider: 'v8',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: [{ find: '@', replacement: resolve(__dirname, './frontend') }],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user