Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions admin-ui/app/redux/features/authSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const authSlice = createSlice({
setAuthState: (state, action: PayloadAction<{ state: boolean }>) => {
state.isAuthenticated = action.payload?.state
},
getUserInfo: (state, _action: PayloadAction<any>) => {},
getUserInfo: (_state, _action: PayloadAction<any>) => {},
getUserInfoResponse: (
state,
action: PayloadAction<{
Expand All @@ -74,7 +74,7 @@ const authSlice = createSlice({
state.isAuthenticated = true
}
},
getAPIAccessToken: (state, _action: PayloadAction<any>) => {},
getAPIAccessToken: (_state, _action: PayloadAction<any>) => {},
getAPIAccessTokenResponse: (
state,
action: PayloadAction<{ access_token?: string; scopes?: string[]; issuer?: string }>,
Expand All @@ -89,7 +89,7 @@ const authSlice = createSlice({
state.isAuthenticated = true
}
},
getUserLocation: (state, _action: PayloadAction<any>) => {},
getUserLocation: (_state, _action: PayloadAction<any>) => {},
getUserLocationResponse: (state, action: PayloadAction<{ location?: Location }>) => {
if (action.payload?.location) {
state.location = action.payload.location
Expand Down
18 changes: 12 additions & 6 deletions admin-ui/app/redux/sagas/AuthSaga.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// @ts-nocheck
/**
* Auth Sagas
*/

import { all, call, fork, put, select, takeEvery } from 'redux-saga/effects'
import {
getOAuth2ConfigResponse,
Expand Down Expand Up @@ -56,13 +54,21 @@ function* getOAuth2ConfigWorker({ payload }) {
yield put(getOAuth2ConfigResponse())
}

function* putConfigWorker({ payload }) {
export function* putConfigWorker({ payload }) {
try {
// Extract metadata (if any) from payload
const { _meta, ...configData } = payload
const token = yield select((state) => state.authReducer.token.access_token)
const response = yield call(putServerConfiguration, { token, props: payload })
const response = yield call(putServerConfiguration, { token, props: configData })
if (response) {
yield put(getOAuth2ConfigResponse({ config: response }))
yield put(updateToast(true, 'success'))

// If cedarlingLogType changed, show specific toast; otherwise show generic success
if (_meta?.cedarlingLogTypeChanged && _meta?.toastMessage) {
yield put(updateToast(true, 'success', _meta.toastMessage))
} else {
yield put(updateToast(true, 'success'))
}
return
}
} catch (error) {
Expand Down
249 changes: 249 additions & 0 deletions admin-ui/app/redux/sagas/__tests__/AuthSaga.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { expectSaga } from 'redux-saga-test-plan'
import * as matchers from 'redux-saga-test-plan/matchers'
import { throwError } from 'redux-saga-test-plan/providers'
import { getOAuth2ConfigResponse, putConfigWorkerResponse } from '../../features/authSlice'
import { updateToast } from '../../features/toastSlice'
import { putServerConfiguration } from '../../api/backend-api'
import * as AuthSaga from '../AuthSaga'

const { putConfigWorker } = AuthSaga

describe('AuthSaga - putConfigWorker', () => {
const mockToken = 'mock-access-token'
const mockConfig = {
sessionTimeoutInMins: 30,
acrValues: 'simple_password_auth',
cedarlingLogType: 'OFF',
additionalParameters: [],
}
const mockResponse = {
...mockConfig,
postLogoutRedirectUri: 'https://example.com/logout',
}

const mockState = {
authReducer: {
token: {
access_token: mockToken,
},
},
}

describe('when cedarlingLogType changes', () => {
// eslint-disable-next-line jest/expect-expect
it('should dispatch updateToast with custom message on success', () => {
const customToastMessage = 'Please relogin to view cedarling changes'
const action = {
type: 'auth/putConfigWorker',
payload: {
...mockConfig,
_meta: {
cedarlingLogTypeChanged: true,
toastMessage: customToastMessage,
},
},
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'success', customToastMessage))
.put(putConfigWorkerResponse())
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should not dispatch custom toast message on error', () => {
const customToastMessage = 'Please relogin to view cedarling changes'
const mockError = new Error('Network error')
const action = {
type: 'auth/putConfigWorker',
payload: {
...mockConfig,
_meta: {
cedarlingLogTypeChanged: true,
toastMessage: customToastMessage,
},
},
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]])
.not.put(updateToast(true, 'success', customToastMessage))
.put(updateToast(true, 'error'))
.put(putConfigWorkerResponse())
.run()
})
})

describe('when cedarlingLogType does not change', () => {
// eslint-disable-next-line jest/expect-expect
it('should dispatch generic success toast when no metadata provided', () => {
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'success'))
.put(putConfigWorkerResponse())
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should dispatch generic success toast when cedarlingLogTypeChanged is false', () => {
const action = {
type: 'auth/putConfigWorker',
payload: {
...mockConfig,
_meta: {
cedarlingLogTypeChanged: false,
},
},
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'success'))
.put(putConfigWorkerResponse())
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should dispatch generic success toast when toastMessage is undefined', () => {
const action = {
type: 'auth/putConfigWorker',
payload: {
...mockConfig,
_meta: {
cedarlingLogTypeChanged: true,
toastMessage: undefined,
},
},
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'success'))
.put(putConfigWorkerResponse())
.run()
})
})

describe('error handling', () => {
// eslint-disable-next-line jest/expect-expect
it('should dispatch error toast on failure', () => {
const mockError = new Error('API Error')
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]])
.not.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'error'))
.put(putConfigWorkerResponse())
.run()
})
})

describe('metadata extraction', () => {
// eslint-disable-next-line jest/expect-expect
it('should not send _meta to the API', () => {
const action = {
type: 'auth/putConfigWorker',
payload: {
...mockConfig,
_meta: {
cedarlingLogTypeChanged: true,
toastMessage: 'Test message',
},
},
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.call(putServerConfiguration, {
token: mockToken,
props: mockConfig, // Should NOT include _meta
})
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should handle payload without _meta correctly', () => {
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.call(putServerConfiguration, {
token: mockToken,
props: mockConfig,
})
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.put(updateToast(true, 'success'))
.run()
})
})

describe('response handling', () => {
// eslint-disable-next-line jest/expect-expect
it('should handle successful response with config update', () => {
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(getOAuth2ConfigResponse({ config: mockResponse }))
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should always call putConfigWorkerResponse in finally block', () => {
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), mockResponse]])
.put(putConfigWorkerResponse())
.run()
})

// eslint-disable-next-line jest/expect-expect
it('should call putConfigWorkerResponse even on error', () => {
const mockError = new Error('API Error')
const action = {
type: 'auth/putConfigWorker',
payload: mockConfig,
}

return expectSaga(putConfigWorker, action)
.withState(mockState)
.provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]])
.put(putConfigWorkerResponse())
.run()
})
})
})
8 changes: 8 additions & 0 deletions admin-ui/app/routes/Apps/Gluu/GluuCommitFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ interface GluuCommitFooterProps {
save?: boolean
back?: boolean
}
disableButtons?: {
save?: boolean
back?: boolean
}
type?: 'button' | 'submit'
disableBackButton?: boolean
cancelHandler?: () => void
Expand All @@ -25,6 +29,7 @@ function GluuCommitFooter({
saveHandler,
extraLabel,
hideButtons,
disableButtons,
type = 'button',
backButtonLabel,
backButtonHandler,
Expand Down Expand Up @@ -54,6 +59,7 @@ function GluuCommitFooter({
type="button"
onClick={disableBackButton ? cancelHandler : goBack}
className="d-flex m-1 mx-5"
disabled={disableButtons?.back}
>
{!disableBackButton && <i className="fa fa-arrow-circle-left me-2"></i>}
{backButtonLabel || t('actions.cancel')}
Expand Down Expand Up @@ -84,6 +90,7 @@ function GluuCommitFooter({
color={`primary-${selectedTheme}`}
style={{ ...applicationStyle.buttonStyle, ...applicationStyle.buttonFlexIconStyles }}
className="ms-auto px-4"
disabled={disableButtons?.save}
>
<i className="fa fa-check-circle me-2"></i>
{t('actions.apply')}
Expand All @@ -97,6 +104,7 @@ function GluuCommitFooter({
style={{ ...applicationStyle.buttonStyle, ...applicationStyle.buttonFlexIconStyles }}
className="ms-auto px-4"
onClick={saveHandler}
disabled={disableButtons?.save}
>
<i className="fa fa-check-circle me-2"></i>
{t('actions.apply')}
Expand Down
45 changes: 45 additions & 0 deletions admin-ui/app/utils/pagingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const getPagingSize = (defaultSize: number = 10): number => {
// Guard against SSR/test environments where localStorage is unavailable
if (typeof window === 'undefined' || !window.localStorage) {
return defaultSize
}

try {
const stored = localStorage.getItem('gluu.pagingSize')

if (!stored) return defaultSize

const parsed = parseInt(stored, 10)
// Only return if it's a valid positive integer (>0)
if (!isNaN(parsed) && parsed > 0) {
return parsed
}

return defaultSize
} catch (error) {
// Silently handle localStorage errors (quota exceeded, privacy mode, etc.)
console.warn('Failed to read paging size from localStorage:', error)
return defaultSize
}
}

export const savePagingSize = (size: number): void => {
// Validate and coerce input to a positive integer
const validSize = Math.floor(size)
if (validSize <= 0) {
console.warn('Invalid paging size:', size, '- must be a positive integer')
return
}

// Guard against SSR/test environments where localStorage is unavailable
if (typeof window === 'undefined' || !window.localStorage) {
return
}

try {
localStorage.setItem('gluu.pagingSize', String(validSize))
} catch (error) {
// Silently handle localStorage errors (quota exceeded, privacy mode, etc.)
console.warn('Failed to save paging size to localStorage:', error)
}
}
Loading
Loading