diff --git a/apps/sensenet/src/context/sn-auth-repository-provider.tsx b/apps/sensenet/src/context/sn-auth-repository-provider.tsx index 8caf2c7fb..55f8724c3 100644 --- a/apps/sensenet/src/context/sn-auth-repository-provider.tsx +++ b/apps/sensenet/src/context/sn-auth-repository-provider.tsx @@ -24,11 +24,21 @@ export function SnAuthRepositoryProvider({ children }: { children: React.ReactNo config: null, }) const repoFromUrl = useQuery().get('repoUrl') - const configString = window.localStorage.getItem(authConfigKey) + const cancelledLogin = useQuery().get('cancelledLogin') + const [configString, setConfigString] = useState() const [authServerUrl, setAuthServerUrl] = useState() const clearState = useCallback(() => setAuthState({ repoUrl: '', config: null }), []) + useEffect(() => { + if (cancelledLogin) { + window.localStorage.removeItem(authConfigKey) + setAuthState((oldState) => ({ ...oldState, repoUrl: '' })) + } else { + setConfigString(window.localStorage.getItem(authConfigKey)) + } + }, [cancelledLogin]) + useEffect(() => { if (configString) { const prevAuthConfig = JSON.parse(configString) @@ -56,8 +66,15 @@ export function SnAuthRepositoryProvider({ children }: { children: React.ReactNo try { setIsLoginInProgress(true) const config = await getAuthConfig(authState.repoUrl) - window.localStorage.setItem(authConfigKey, JSON.stringify(config)) - setAuthState((oldState) => ({ ...oldState, config: config.userManagerSettings })) + if (config.authServerSettings.type === 'SNAuth') { + window.localStorage.setItem(authConfigKey, JSON.stringify(config)) + setConfigString(window.localStorage.getItem(authConfigKey)) + setAuthState((oldState) => ({ ...oldState, config: config.userManagerSettings })) + } else { + logger.error({ message: 'Incompatible authentication server type' }) + window.localStorage.removeItem(authConfigKey) + setAuthState((oldState) => ({ ...oldState, repoUrl: '' })) + } } catch (error) { logger.warning({ data: error, message: `Couldn't connect to ${authState.repoUrl}` }) window.localStorage.removeItem(authConfigKey) @@ -71,7 +88,7 @@ export function SnAuthRepositoryProvider({ children }: { children: React.ReactNo getConfig() }, [getConfig]) - if (!authState.config || !authState.repoUrl) { + if (!authState.config || !authState.repoUrl || !authServerUrl) { return (
@@ -101,6 +118,13 @@ export function SnAuthRepositoryProvider({ children }: { children: React.ReactNo repoUrl={authState.repoUrl} snAuthConfiguration={{ callbackUri: '/authentication/callback', + }} + eventCallbacks={{ + onLogout() { + setConfigString(null) + window.localStorage.removeItem(authConfigKey) + clearState() + }, }}> {children} @@ -165,18 +189,17 @@ const RepoProvider = ({ useEffect(() => { ;(async () => { const configString = window.localStorage.getItem(authConfigKey) - if (!user && !isLoading && !accessToken && configString) { + if (!user && !isLoading && !accessToken && authServerUrl && configString) { try { await externalLogin() } catch (error) { - const config = JSON.parse(configString) - logger.error({ data: error, message: `Couldn't connect to ${config.authority}` }) + logger.error({ data: error, message: `Couldn't connect to ${authServerUrl}` }) window.localStorage.removeItem(authConfigKey) clearAuthState() } } })() - }, [clearAuthState, logger, externalLogin, logout, user, isLoading, accessToken]) + }, [clearAuthState, logger, externalLogin, logout, user, isLoading, accessToken, authServerUrl]) if (!user || !repo) { return null diff --git a/packages/sn-auth-react/src/components/auth-routes.tsx b/packages/sn-auth-react/src/components/auth-routes.tsx index 914a2ea42..4937e4334 100644 --- a/packages/sn-auth-react/src/components/auth-routes.tsx +++ b/packages/sn-auth-react/src/components/auth-routes.tsx @@ -1,4 +1,4 @@ -import React, { memo, ReactNode } from 'react' +import React, { memo, ReactNode, useEffect } from 'react' import { Authenticating } from './authenticating' export type AuthRoutesProps = { @@ -8,6 +8,10 @@ export type AuthRoutesProps = { } const AuthRoutesComponent = ({ callbackUri, children, currentPath }: AuthRoutesProps) => { + useEffect(() => { + console.log(currentPath) + }, [currentPath]) + switch (currentPath) { case callbackUri: return diff --git a/packages/sn-auth-react/src/components/authentication-provider.tsx b/packages/sn-auth-react/src/components/authentication-provider.tsx index a84aa2d91..9db90998a 100644 --- a/packages/sn-auth-react/src/components/authentication-provider.tsx +++ b/packages/sn-auth-react/src/components/authentication-provider.tsx @@ -2,34 +2,43 @@ import React, { createContext, ReactNode, useState, useEffect } from 'react' import { User } from '../models/user' import { AuthRoutes } from './auth-routes' import { SnAuthConfiguration } from '../models/sn-auth-configuration' -import { changePasswordApiCall, convertAuthTokenApiCall, forgotPasswordApiCall, getUserDetailsApiCall, loginApiCall, logoutApiCall, multiFactorApiCall, passwordRecoveryApiCall, refreshTokenApiCall, validateTokenApiCall } from '../server-actions' +import { + changePasswordApiCall, + convertAuthTokenApiCall, + forgotPasswordApiCall, + getUserDetailsApiCall, + loginApiCall, + logoutApiCall, + multiFactorApiCall, + passwordRecoveryApiCall, + refreshTokenApiCall, + validateTokenApiCall, +} from '../server-actions' import { getAccessToken, getRefreshToken, - getUserDetails, removeAccessToken, removeRefreshToken, - removeUserDetails, setAccessToken as setAccessTokenStorage, setRefreshToken as setRefreshTokenStorage, - setUserDetails as setUserDetailsStorage, -} from '../storageHelpers' +} from '../storage-helpers' import { LoginRequest } from '../models/login-request' import { LoginResponse } from '../models/login-response' import { MultiFactorLoginRequest } from '../models/multi-factor-login-request' +import { isTokenAboutToExpire } from '../token-helpers' export interface AuthenticationContextState { isLoading: boolean - user: User | null + user?: User login: (loginRequest: LoginRequest) => Promise externalLogin: () => void multiFactorLogin: (multiFactorRequest: MultiFactorLoginRequest) => void - forgotPassword: (email: string) => Promise, - passwordRecovery: (token: string, password: string) => Promise, + forgotPassword: (email: string) => Promise + passwordRecovery: (token: string, password: string) => Promise changePassword: (password: string) => Promise logout: () => void - accessToken: string | null - error: string | null + accessToken?: string + error?: string } export const AuthenticationContext = createContext(undefined) @@ -39,48 +48,26 @@ export interface AuthenticationProviderProps { snAuthConfiguration: SnAuthConfiguration repoUrl: string authServerUrl: string -} - -const TOKEN_EXPIRY_THRESHOLD = 10 * 1000 - -const parseJwt = (token: string) => { - try { - const base64Url = token.split('.')[1] - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent( - window - .atob(base64) - .split('') - .map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) - }) - .join(''), - ) - return JSON.parse(jsonPayload) - } catch (error) { - console.error('Failed to parse JWT', error) - return null + eventCallbacks?: { + onInitialized?: () => void + onNoInitialization?: () => void + onLogout?: () => void } } -const isTokenAboutToExpire = (token: string | null) => { - if (!token) return true - const decoded = parseJwt(token) - if (!decoded || !decoded.exp) return true - - const expiryTime = decoded.exp * 1000 - const currentTime = Date.now() - return expiryTime - currentTime < TOKEN_EXPIRY_THRESHOLD +export interface AuthState { + accessToken?: string + refreshToken?: string + user?: User + error?: string + isLoading: boolean } +const TOKEN_EXPIRY_THRESHOLD = 10 * 1000 + export const AuthenticationProvider = (props: AuthenticationProviderProps) => { - const [accessToken, setAccessToken] = useState(getAccessToken()) - const [refreshToken, setRefreshToken] = useState(getRefreshToken()) - const [user, setUser] = useState(getUserDetails()) + const [authState, setState] = useState({ isLoading: true }) const [path, setPath] = useState(window.location.pathname) - const [isLoading, setIsLoading] = useState(true) - const [isRefreshingToken, setIsRefreshingToken] = useState(false) - const [error, setError] = useState(null) const setNewPath = () => setPath(window.location.pathname) @@ -92,72 +79,95 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => { useEffect(() => { if (path === props.snAuthConfiguration.callbackUri) { convertAuthToken().finally(() => { - window.location.replace('/') + window.history.pushState({}, '', '/') + setNewPath() }) } }, [path]) - useEffect(() => { const validateAndRefreshToken = async () => { - setIsLoading(true); - try { - if (accessToken) { - let accessTokenLocal = accessToken - const isValid = await validateTokenApiCall(props.authServerUrl, accessTokenLocal); - - if (!isValid) { - const response = await refreshAccessToken(); - if (!response?.accessTokenResponse) - throw new Error() - - accessTokenLocal = response.accessTokenResponse + if (path !== props.snAuthConfiguration.callbackUri) { + setState({ isLoading: true }) + try { + let accessToken = getAccessToken() + let refreshToken = getRefreshToken() + if (accessToken && refreshToken) { + const isValid = await validateTokenApiCall(props.authServerUrl, accessToken) + + if (!isValid) { + const response = await refreshAccessToken(refreshToken) + if (!response?.accessToken) throw new Error() + + accessToken = response.accessToken + refreshToken = response.refreshToken + setAccessAndRefreshTokenStorage(accessToken, refreshToken) + } + + const userDetails = await getUserDetailsApiCall(props.repoUrl, accessToken) + setState({ + accessToken: accessToken, + refreshToken: refreshToken, + user: userDetails, + isLoading: false, + }) + props.eventCallbacks?.onInitialized?.() + } else { + props.eventCallbacks?.onNoInitialization?.() + setState({ + isLoading: false, + }) } - - const userDetails = await getUserDetailsApiCall(props.authServerUrl, accessTokenLocal); - setUser(userDetails); + } catch (err) { + setState({ + error: 'Failed to validate or refresh token', + isLoading: false, + }) + logoutLocal() } - - setIsLoading(false); - } catch (err) { - setError('Failed to validate or refresh token'); - setIsLoading(false); } - }; + } - validateAndRefreshToken(); - }, []); + validateAndRefreshToken() + }, []) useEffect(() => { const intervalId = setInterval(async () => { - const accToken = getAccessToken() - if (accToken && isTokenAboutToExpire(accToken) && !isRefreshingToken) { - setIsRefreshingToken(true) - } - }, TOKEN_EXPIRY_THRESHOLD); - - return () => clearInterval(intervalId); - }, [isRefreshingToken]) - - useEffect(() => { - const refreshToken = async () => { - try { - const response = await refreshAccessToken(); - if (!response?.accessTokenResponse) - throw new Error() - - const userDetails = await getUserDetailsApiCall(props.authServerUrl, response.accessTokenResponse); - setUser(userDetails); - } catch (err) { - setError('Failed to refresh access token'); - logoutLocal(); + if ( + authState.accessToken && + authState.refreshToken && + isTokenAboutToExpire(authState.accessToken, TOKEN_EXPIRY_THRESHOLD) && + !authState.isLoading && + !authState.error + ) { + setState({ + ...authState, + isLoading: true, + }) + try { + const response = await refreshAccessToken(authState.refreshToken) + if (!response?.accessToken) throw new Error() + + const userDetails = await getUserDetailsApiCall(props.repoUrl, response.accessToken) + setState({ + accessToken: response.accessToken, + refreshToken: response.refreshToken, + user: userDetails, + isLoading: false, + }) + setAccessAndRefreshTokenStorage(response.accessToken, response.refreshToken) + } catch (err) { + setState({ + error: 'Failed to refresh token', + isLoading: false, + }) + logoutLocal() + } } - setIsRefreshingToken(false) - } + }, TOKEN_EXPIRY_THRESHOLD) - if (isRefreshingToken) - refreshToken() - }, [isRefreshingToken, props.authServerUrl]) + return () => clearInterval(intervalId) + }, [authState]) const externalLogin = () => { window.location.replace( @@ -166,112 +176,131 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => { } const convertAuthToken = async () => { - setIsLoading(true) const urlParams = new URLSearchParams(window.location.search) const authToken = urlParams.get('auth_code') + setState({ + ...authState, + isLoading: true, + }) try { if (authToken) { - const { accessToken: accessTokenResponse, refreshToken: refreshTokenResponse } = await convertAuthTokenApiCall( - props.authServerUrl, - authToken, - ) - setAccessAndRefreshToken(accessTokenResponse, refreshTokenResponse) - - const user = await getUserDetailsApiCall(props.authServerUrl, accessTokenResponse) - setUser(user) - setUserDetailsStorage(user) + const { accessToken, refreshToken } = await convertAuthTokenApiCall(props.authServerUrl, authToken) + + const user = await getUserDetailsApiCall(props.repoUrl, accessToken) + setState({ + user, + accessToken: accessToken, + refreshToken: refreshToken, + isLoading: false, + }) + setAccessAndRefreshTokenStorage(accessToken, refreshToken) } } catch (e) { - setError(e); - console.error(e) - } finally { - setIsLoading(false) + setState({ + error: 'Failed to convert auth token', + isLoading: false, + }) } } - const refreshAccessToken = async () => { - setIsLoading(true) - if (refreshToken) { + const refreshAccessToken = async (refToken: string) => { + if (refToken) { try { - const { accessToken: accessTokenResponse, refreshToken: refreshTokenResponse } = await refreshTokenApiCall( - props.authServerUrl, - refreshToken, - ) - setAccessAndRefreshToken(accessTokenResponse, refreshTokenResponse) + setState({ + ...authState, + isLoading: true, + }) + const { accessToken, refreshToken } = await refreshTokenApiCall(props.authServerUrl, refToken) - return { accessTokenResponse, refreshTokenResponse } + return { accessToken, refreshToken } } catch (e) { console.error(e) - setError("Failed to refresh token") + setState({ + ...authState, + error: 'Failed to refresh access token', + isLoading: false, + }) logoutLocal() - } finally { - setIsLoading(false) } - } else { - setIsLoading(false) } } const logout = () => { - if (accessToken && props.authServerUrl) - logoutApiCall(props.authServerUrl, accessToken) + if (authState.accessToken && props.authServerUrl) + logoutApiCall(props.authServerUrl, authState.accessToken) .catch((e) => { console.error(e) }) .finally(() => { + props.eventCallbacks?.onLogout?.() logoutLocal() }) else logoutLocal() } const login = async (loginRequest: LoginRequest): Promise => { + setState({ + ...authState, + isLoading: true, + }) try { const response = await loginApiCall(props.authServerUrl, loginRequest) if (!response.multiFactorRequired && response.accessToken && response.refreshToken) { - setAccessAndRefreshToken(response.accessToken, response.refreshToken) - - const user = await getUserDetailsApiCall(props.authServerUrl, response.accessToken) - setUser(user) - setUserDetailsStorage(user) - - return response - } - else { - throw new Error() + const user = await getUserDetailsApiCall(props.repoUrl, response.accessToken) + setState({ + user, + accessToken: response.accessToken, + refreshToken: response.refreshToken, + isLoading: false, + }) + setAccessAndRefreshTokenStorage(response.accessToken, response.refreshToken) } - } - catch (e) { - console.log("Error during login.") - removeAccessToken() - removeRefreshToken() + return response + } catch (e) { + setState({ + ...authState, + error: 'Error during login.', + isLoading: false, + }) + logoutLocal() - throw e; + throw e } } const multiFactorLogin = async (multiFactorRequest: MultiFactorLoginRequest): Promise => { try { + setState({ + ...authState, + isLoading: true, + }) const response = await multiFactorApiCall(props.authServerUrl, multiFactorRequest) if (response.accessToken && response.refreshToken) { - setAccessAndRefreshToken(response.accessToken, response.refreshToken) + const user = await getUserDetailsApiCall(props.repoUrl, response.accessToken) + setState({ + user, + accessToken: response.accessToken, + refreshToken: response.refreshToken, + isLoading: false, + }) + setAccessAndRefreshTokenStorage(response.accessToken, response.refreshToken) - return response; - } - else { - throw new Error(); + return response + } else { + throw new Error() } - } - catch (e) { - console.log("Error during multi-factor validation.") - - removeAccessToken() - removeRefreshToken() + } catch (e) { + setState({ + ...authState, + error: 'Error during multi-factor validation.', + }) + logoutLocal() - throw e; + throw e } } @@ -284,26 +313,24 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => { } const changePassword = async (password: string) => { - if (accessToken) - changePasswordApiCall(props.authServerUrl, accessToken, { password }) + if (authState.accessToken) changePasswordApiCall(props.authServerUrl, authState.accessToken, { password }) } const logoutLocal = () => { - setAccessToken(null) - setRefreshToken(null) - setUser(null) + setState({ + ...authState, + accessToken: undefined, + user: undefined, + refreshToken: undefined, + }) removeAccessToken() removeRefreshToken() - removeUserDetails() - window.location.replace('/') + window.history.pushState({}, '', '/') } - const setAccessAndRefreshToken = (accessToken: string, refreshToken: string) => { - setAccessToken(accessToken) - setRefreshToken(refreshToken) - + const setAccessAndRefreshTokenStorage = (accessToken: string, refreshToken: string) => { setAccessTokenStorage(accessToken) setRefreshTokenStorage(refreshToken) } @@ -311,8 +338,10 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps) => { return ( { passwordRecovery, changePassword, multiFactorLogin, - isLoading, - error }}> {props.children} diff --git a/packages/sn-auth-react/src/constants.ts b/packages/sn-auth-react/src/constants.ts index a51815a79..ce5924712 100644 --- a/packages/sn-auth-react/src/constants.ts +++ b/packages/sn-auth-react/src/constants.ts @@ -1,3 +1,2 @@ export const ACCESS_TOKEN_KEY = "sn-auth-access-token" export const REFRESH_TOKEN_KEY = "sn-auth-refresh-token" -export const USER_DETAILS_KEY = "sn-auth-user-details" diff --git a/packages/sn-auth-react/src/server-actions.ts b/packages/sn-auth-react/src/server-actions.ts index 432a3134a..07a743f30 100644 --- a/packages/sn-auth-react/src/server-actions.ts +++ b/packages/sn-auth-react/src/server-actions.ts @@ -142,7 +142,7 @@ export async function passwordRecoveryApiCall(server: string, passwordRequest: P export async function changePasswordApiCall(server: string, accessToken: string, passwordRequest: ChangePasswordRequest): Promise { try { - const response = await fetch(`${server}/api/auth/password-recovery`, { + const response = await fetch(`${server}/api/auth/change-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -183,7 +183,7 @@ export async function multiFactorApiCall(server: string, loginRequest: MultiFact export async function validateTokenApiCall(server: string, accessToken: string): Promise { try { - const response = await fetch(`${server}/api/user`, { + const response = await fetch(`${server}/api/auth/validate-token`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -200,9 +200,9 @@ export async function validateTokenApiCall(server: string, accessToken: string): return false } -export async function getUserDetailsApiCall(server: string, accessToken: string): Promise { +export async function getUserDetailsApiCall(repoUrl: string, accessToken: string): Promise { try { - const response = await fetch(`${server}/api/user`, { + const response = await fetch(`${repoUrl}/odata.svc/('Root')/GetCurrentUser`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -211,22 +211,11 @@ export async function getUserDetailsApiCall(server: string, accessToken: string) }) if (response.ok) { - const user = await response.json() - - return { - Avatar: { - Url: user.avatar?.url - }, - DisplayName: user.displayName, - Name: user.name, - Email: user.email, - FullName: user.fullName, - Id: user.id, - LoginName: user.loginName, - Path: user.path - } + const responseJson = await response.json() + + return responseJson.d; } else { - throw new Error(`Error during logout: ${response.statusText}`) + throw new Error(`Error during getting user: ${response.statusText}`) } } catch (error) { console.error('Error:', error) diff --git a/packages/sn-auth-react/src/storageHelpers.ts b/packages/sn-auth-react/src/storage-helpers.ts similarity index 55% rename from packages/sn-auth-react/src/storageHelpers.ts rename to packages/sn-auth-react/src/storage-helpers.ts index cd3a0b376..3dbaee4ed 100644 --- a/packages/sn-auth-react/src/storageHelpers.ts +++ b/packages/sn-auth-react/src/storage-helpers.ts @@ -1,5 +1,4 @@ -import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_DETAILS_KEY } from "./constants" -import { User } from "./models/user" +import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "./constants" export const getAccessToken = () : string | null => { return window.localStorage.getItem(ACCESS_TOKEN_KEY) @@ -24,21 +23,3 @@ export const setRefreshToken = (token: string) : void => { export const removeRefreshToken = () : void => { window.localStorage.removeItem(REFRESH_TOKEN_KEY) } - -export const getUserDetails = () : User | null => { - const user = window.localStorage.getItem(USER_DETAILS_KEY) - - if (user) - return JSON.parse(user) - return null -} - - -export const setUserDetails = (userDetails: User) : void => { - window.localStorage.setItem(USER_DETAILS_KEY, JSON.stringify(userDetails)) -} - -export const removeUserDetails = () : void => { - window.localStorage.removeItem(USER_DETAILS_KEY) -} - diff --git a/packages/sn-auth-react/src/token-helpers.ts b/packages/sn-auth-react/src/token-helpers.ts new file mode 100644 index 000000000..5c16c2dfe --- /dev/null +++ b/packages/sn-auth-react/src/token-helpers.ts @@ -0,0 +1,29 @@ +const parseJwt = (token: string) => { + try { + const base64Url = token.split('.')[1] + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join(''), + ) + return JSON.parse(jsonPayload) + } catch (error) { + console.error('Failed to parse JWT', error) + return null + } + } + + export const isTokenAboutToExpire = (token: string | null, threshold: number) => { + if (!token) return true + const decoded = parseJwt(token) + if (!decoded || !decoded.exp) return true + + const expiryTime = decoded.exp * 1000 + const currentTime = Date.now() + return expiryTime - currentTime < threshold + } \ No newline at end of file diff --git a/packages/sn-auth-react/test/authentication-provider.test.tsx b/packages/sn-auth-react/test/authentication-provider.test.tsx deleted file mode 100644 index 79690c3a6..000000000 --- a/packages/sn-auth-react/test/authentication-provider.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react' -import { render, screen, act, waitFor } from '@testing-library/react' -import { AuthenticationProvider, AuthenticationContext } from '../src/components/authentication-provider' -import { AuthRoutes } from '../src/components/auth-routes' -import { SnAuthConfiguration } from '../src/models/sn-auth-configuration' -import { User } from '../src/models/user' -import { - convertAuthTokenApiCall, - getUserDetailsApiCall, - logoutApiCall, - refreshTokenApiCall, -} from '../src/server-actions' -import { - getAccessToken, - getRefreshToken, - getUserDetails, - setAccessToken as setAccessTokenStorage, - setRefreshToken as setRefreshTokenStorage, - setUserDetails as setUserDetailsStorage, - removeAccessToken, - removeRefreshToken, - removeUserDetails, -} from '../src/storageHelpers' - -jest.mock('../src/server-actions', () => ({ - convertAuthTokenApiCall: jest.fn(), - getUserDetailsApiCall: jest.fn(), - logoutApiCall: jest.fn(), - refreshTokenApiCall: jest.fn(), -})) - -jest.mock('../src/storageHelpers', () => ({ - getAccessToken: jest.fn(), - getRefreshToken: jest.fn(), - getUserDetails: jest.fn(), - setAccessToken: jest.fn(), - setRefreshToken: jest.fn(), - setUserDetails: jest.fn(), - removeAccessToken: jest.fn(), - removeRefreshToken: jest.fn(), - removeUserDetails: jest.fn(), -})) - -const mockSnAuthConfiguration: SnAuthConfiguration = { - callbackUri: '/callback', -} - -const mockUser: User = { - Id: 1, - LoginName: 'testuser', - FullName: 'testuser', - DisplayName: 'testuser', - Name: 'testuser', - Email: 'testuser', - Avatar: { - Url: 'testuser', - }, - Path: 'testuser', -} - -const setup = (children?: React.ReactNode) => { - render( - - {children ||
Content
} -
, - ) -} - -describe('AuthenticationProvider', () => { - let originalLocation: Location - - beforeEach(() => { - jest.clearAllMocks() - ; (getAccessToken as jest.Mock).mockReturnValue('mockAccessToken') - ; (getRefreshToken as jest.Mock).mockReturnValue('mockRefreshToken') - ; (getUserDetails as jest.Mock).mockReturnValue(mockUser) - - originalLocation = window.location - - Object.defineProperty(window, 'location', { - writable: true, - value: { - ...originalLocation, - replace: jest.fn(), - pathname: '/', - search: '', - }, - }) - }) - - afterEach(() => { - // Restore the original window location - window.location = originalLocation - }) - - test('renders children correctly', () => { - setup() - expect(screen.getByText('Content')).toBeInTheDocument() - }) - - test('calls login function and redirects to the correct URL', async () => { - setup( - - {(c) => } - , - ) - - await act(() => { - screen.getByText('Login').click() - }) - - expect(window.location.replace).toHaveBeenCalledWith( - 'http://authserver.com/Login?RedirectUrl=http://localhost&CallbackUri=/callback', - ) - }) - - test('handles logout correctly', async () => { - setup( - - {(c) => } - , - ) - ; (logoutApiCall as jest.Mock).mockResolvedValueOnce(null) - - await act(async () => { - screen.getByText('Logout').click() - }) - - expect(logoutApiCall).toHaveBeenCalledWith('http://authserver.com', 'mockAccessToken') - expect(removeAccessToken).toHaveBeenCalled() - expect(removeRefreshToken).toHaveBeenCalled() - expect(removeUserDetails).toHaveBeenCalled() - }) - - test('refreshes access token when about to expire', async () => { - ; (refreshTokenApiCall as jest.Mock).mockResolvedValue({ - accessToken: 'newAccessToken', - refreshToken: 'newRefreshToken', - }) - - await act(async () => { - setup() - }) - - await waitFor(() => { - expect(refreshTokenApiCall).toHaveBeenCalledWith('http://authserver.com', 'mockRefreshToken') - expect(setAccessTokenStorage).toHaveBeenCalledWith('newAccessToken') - expect(setRefreshTokenStorage).toHaveBeenCalledWith('newRefreshToken') - }) - }) - - test('calls convertAuthToken on callback path', async () => { - // Mock window.location for callback path scenario - Object.defineProperty(window, 'location', { - writable: true, - value: { - ...originalLocation, - pathname: '/callback', - search: '?auth_code=testAuthToken', - replace: jest.fn(), - }, - }) - ; (convertAuthTokenApiCall as jest.Mock).mockResolvedValue({ - accessToken: 'newAccessToken', - refreshToken: 'newRefreshToken', - }) - ; (getUserDetailsApiCall as jest.Mock).mockResolvedValue(mockUser) - - await act(async () => { - setup() - }) - - expect(convertAuthTokenApiCall).toHaveBeenCalledWith('http://authserver.com', 'testAuthToken') - expect(setAccessTokenStorage).toHaveBeenCalledWith('newAccessToken') - expect(setRefreshTokenStorage).toHaveBeenCalledWith('newRefreshToken') - expect(setUserDetailsStorage).toHaveBeenCalledWith(mockUser) - expect(window.location.replace).toHaveBeenCalledWith('/') - }) -}) diff --git a/packages/sn-auth-react/test/setupTests.ts b/packages/sn-auth-react/test/setupTests.ts deleted file mode 100644 index 010b0b5d4..000000000 --- a/packages/sn-auth-react/test/setupTests.ts +++ /dev/null @@ -1 +0,0 @@ -import '@testing-library/jest-dom' \ No newline at end of file