Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d569e30
feat(admin-ui): revamp User Claims module as per Figma (#2632)
faisalsiddique4400 Mar 16, 2026
50c48c2
feat(admin-ui): merge main branch into user claims
faisalsiddique4400 Mar 16, 2026
5ff9e8a
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
bcc8b6b
Merge branch 'main' into admin-ui-issue-2632
faisalsiddique4400 Mar 17, 2026
064823c
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
391ec54
Merge branch 'admin-ui-issue-2632' of github-faisal:GluuFederation/fl…
faisalsiddique4400 Mar 17, 2026
8d344f0
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
2d4ae86
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
97f4b8e
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
0629b93
feat(admin-ui): resolve code rabbit issues
faisalsiddique4400 Mar 17, 2026
fe2251c
jans lock theme fixes
faisalsiddique4400 Mar 17, 2026
892c3a9
test files added
faisalsiddique4400 Mar 17, 2026
52468df
test files added
faisalsiddique4400 Mar 18, 2026
c5fd604
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
3986e98
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
e2f7cfc
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
f77f6fc
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
02a5d19
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
d29301a
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
a026bae
code rabbit fixes
faisalsiddique4400 Mar 18, 2026
8dc8ccf
code rabbit fixes
faisalsiddique4400 Mar 19, 2026
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
53 changes: 53 additions & 0 deletions admin-ui/app/cedarling/__tests__/client/CedarlingClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { cedarlingClient } from '@/cedarling/client/CedarlingClient'
import type { TokenAuthorizationRequest } from '@/cedarling'
import initWasm, { init } from '@janssenproject/cedarling_wasm'

// The WASM module is mocked via __mocks__/@janssenproject/cedarling_wasm.ts

describe('cedarlingClient', () => {
it('exports initialize and token_authorize methods', () => {
expect(typeof cedarlingClient.initialize).toBe('function')
expect(typeof cedarlingClient.token_authorize).toBe('function')
})

describe('initialize', () => {
it('initializes without error', async () => {
await expect(cedarlingClient.initialize({})).resolves.toBeUndefined()
})

it('does not re-initialize when already initialized', async () => {
await cedarlingClient.initialize({})
const initWasmCallCount = (initWasm as jest.Mock).mock.calls.length
const initCallCount = (init as jest.Mock).mock.calls.length

await expect(cedarlingClient.initialize({})).resolves.toBeUndefined()

expect((initWasm as jest.Mock).mock.calls).toHaveLength(initWasmCallCount)
expect((init as jest.Mock).mock.calls).toHaveLength(initCallCount)
})
})

describe('token_authorize', () => {
const request: TokenAuthorizationRequest = {
tokens: {
access_token: 'test-access-token',
id_token: 'test-id-token',
userinfo_token: 'test-userinfo-token',
},
action: 'Gluu::Flex::AdminUI::Action::"read"',
resource: {
cedar_entity_mapping: {
entity_type: 'Gluu::Flex::AdminUI::Resources::Features',
id: 'Dashboard',
},
},
context: {},
}

it('returns authorization response after initialization', async () => {
await cedarlingClient.initialize({})
const response = await cedarlingClient.token_authorize(request)
expect(response).toHaveProperty('decision')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CEDAR_RESOURCE_SCOPES, CEDARLING_CONSTANTS } from '@/cedarling/constants/resourceScopes'
import { ADMIN_UI_RESOURCES } from '@/cedarling/utility'
import type { AdminUiFeatureResource } from '@/cedarling'
import {
JANS_LOCK_READ,
JANS_LOCK_WRITE,
SMTP_READ,
SMTP_WRITE,
SMTP_DELETE,
} from '@/utils/PermChecker'

describe('CEDAR_RESOURCE_SCOPES', () => {
const allResources = Object.keys(ADMIN_UI_RESOURCES) as AdminUiFeatureResource[]

it('has an entry for every AdminUiFeatureResource', () => {
allResources.forEach((resource) => {
expect(CEDAR_RESOURCE_SCOPES).toHaveProperty(resource)
})
})

it('has no extra keys beyond AdminUiFeatureResource', () => {
expect(Object.keys(CEDAR_RESOURCE_SCOPES).sort()).toEqual(allResources.sort())
})

it.each(allResources)('%s has non-empty scope entries', (resource) => {
const scopes = CEDAR_RESOURCE_SCOPES[resource]
expect(Array.isArray(scopes)).toBe(true)
expect(scopes.length).toBeGreaterThan(0)
})

it.each(allResources)('%s scope entries have correct resourceId', (resource) => {
const scopes = CEDAR_RESOURCE_SCOPES[resource]
scopes.forEach((scope) => {
expect(scope.resourceId).toBe(resource)
expect(typeof scope.permission).toBe('string')
expect(scope.permission.length).toBeGreaterThan(0)
})
})

it('Lock has read and write scopes', () => {
const lockScopes = CEDAR_RESOURCE_SCOPES.Lock
expect(lockScopes).toHaveLength(2)
const permissions = lockScopes.map((s) => s.permission)
expect(permissions).toContain(JANS_LOCK_READ)
expect(permissions).toContain(JANS_LOCK_WRITE)
})

it('SMTP has read, write, and delete scopes', () => {
const smtpScopes = CEDAR_RESOURCE_SCOPES.SMTP
expect(smtpScopes).toHaveLength(3)
const permissions = smtpScopes.map((s) => s.permission)
expect(permissions).toContain(SMTP_READ)
expect(permissions).toContain(SMTP_WRITE)
expect(permissions).toContain(SMTP_DELETE)
})

it('Dashboard has 2 stat scopes', () => {
const dashScopes = CEDAR_RESOURCE_SCOPES.Dashboard
expect(dashScopes).toHaveLength(2)
})
})

describe('CEDARLING_CONSTANTS', () => {
it('has ACTION_TYPE with correct prefix', () => {
expect(CEDARLING_CONSTANTS.ACTION_TYPE).toBe('Gluu::Flex::AdminUI::Action::')
})

it('has RESOURCE_TYPE with correct value', () => {
expect(CEDARLING_CONSTANTS.RESOURCE_TYPE).toBe('Gluu::Flex::AdminUI::Resources::Features')
})
})
16 changes: 16 additions & 0 deletions admin-ui/app/cedarling/__tests__/enums/CedarlingLogType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType'

describe('CedarlingLogType', () => {
it('has OFF value', () => {
expect(CedarlingLogType.OFF).toBe('off')
})

it('has STD_OUT value', () => {
expect(CedarlingLogType.STD_OUT).toBe('std_out')
})

it('has exactly 2 values', () => {
const values = Object.values(CedarlingLogType)
expect(values).toHaveLength(2)
})
})
230 changes: 230 additions & 0 deletions admin-ui/app/cedarling/__tests__/hooks/useCedarling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { renderHook, act } from '@testing-library/react'
import React from 'react'
import { Provider } from 'react-redux'
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { useCedarling } from '@/cedarling/hooks/useCedarling'
import type { CedarPermissionsState } from '@/cedarling'

jest.mock('@/cedarling/client', () => ({
cedarlingClient: {
initialize: jest.fn().mockResolvedValue(undefined),
token_authorize: jest.fn().mockResolvedValue({ decision: true }),
},
}))

const createStore = (
overrides: {
authState?: Record<string, string | string[] | undefined>
cedarState?: Partial<CedarPermissionsState>
} = {},
) => {
const defaultAuth = {
userinfo_jwt: 'test-userinfo-jwt',
idToken: 'test-id-token',
jwtToken: 'test-access-token',
permissions: [],
...overrides.authState,
}

const defaultCedar: CedarPermissionsState = {
permissions: {},
loading: false,
error: null,
initialized: true,
isInitializing: false,
cedarFailedStatusAfterMaxTries: null,
policyStoreJson: '',
...overrides.cedarState,
}

return configureStore({
reducer: combineReducers({
authReducer: (state = defaultAuth) => state,
cedarPermissions: (state = defaultCedar) => state,
noReducer: (state = {}) => state,
}),
})
}

const createWrapper = (store: ReturnType<typeof createStore>) => {
const Wrapper = ({ children }: { children: React.ReactNode }) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
React.createElement(Provider, { store } as React.ComponentProps<typeof Provider>, children)
return Wrapper
}

describe('useCedarling', () => {
describe('hasCedarReadPermission', () => {
it('returns undefined when no cached permission', () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarReadPermission('Dashboard')).toBeUndefined()
})

it('returns cached read permission', () => {
const store = createStore({
cedarState: { permissions: { 'Dashboard::read': true } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarReadPermission('Dashboard')).toBe(true)
})

it('returns false for denied read permission', () => {
const store = createStore({
cedarState: { permissions: { 'Users::read': false } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarReadPermission('Users')).toBe(false)
})
})

describe('hasCedarWritePermission', () => {
it('returns undefined when no cached permission', () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarWritePermission('Dashboard')).toBeUndefined()
})

it('returns cached write permission', () => {
const store = createStore({
cedarState: { permissions: { 'SMTP::write': true } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarWritePermission('SMTP')).toBe(true)
})
})

describe('hasCedarDeletePermission', () => {
it('returns undefined when no cached permission', () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarDeletePermission('Scripts')).toBeUndefined()
})

it('returns cached delete permission', () => {
const store = createStore({
cedarState: { permissions: { 'Scripts::delete': true } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.hasCedarDeletePermission('Scripts')).toBe(true)
})
})

describe('authorize', () => {
it('returns not authorized when cedarling is not initialized', async () => {
const store = createStore({
cedarState: { initialized: false, isInitializing: true },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let authResult: { isAuthorized: boolean; error?: string } = { isAuthorized: false }
await act(async () => {
authResult = await result.current.authorize([
{ permission: 'https://example.com/read', resourceId: 'Dashboard' },
])
})
expect(authResult.isAuthorized).toBe(false)
expect(authResult.error).toContain('not yet initialized')
})

it('returns not authorized when tokens are missing', async () => {
const store = createStore({
authState: { userinfo_jwt: undefined, idToken: undefined, jwtToken: undefined },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let authResult: { isAuthorized: boolean; error?: string } = { isAuthorized: false }
await act(async () => {
authResult = await result.current.authorize([
{ permission: 'https://example.com/read', resourceId: 'Dashboard' },
])
})
expect(authResult.isAuthorized).toBe(false)
expect(authResult.error).toContain('tokens are missing')
})

it('returns not authorized for empty scope array', async () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let authResult: { isAuthorized: boolean } = { isAuthorized: false }
await act(async () => {
authResult = await result.current.authorize([])
})
expect(authResult.isAuthorized).toBe(false)
})

it('returns cached decision without calling API', async () => {
const store = createStore({
cedarState: { permissions: { 'Dashboard::read': true } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let authResult: { isAuthorized: boolean } = { isAuthorized: false }
await act(async () => {
authResult = await result.current.authorize([
{ permission: 'https://example.com/read', resourceId: 'Dashboard' },
])
})
expect(authResult.isAuthorized).toBe(true)
})
})

describe('authorizeHelper', () => {
it('returns empty array for empty scopes', async () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let results: { isAuthorized: boolean }[] = []
await act(async () => {
results = await result.current.authorizeHelper([])
})
expect(results).toEqual([])
})

it('deduplicates entries with same resourceId and action', async () => {
const store = createStore({
cedarState: { permissions: { 'Dashboard::read': true } },
})
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

let results: { isAuthorized: boolean }[] = []
await act(async () => {
results = await result.current.authorizeHelper([
{ permission: 'https://example.com/stat-read', resourceId: 'Dashboard' },
{ permission: 'https://example.com/stat-jans-read', resourceId: 'Dashboard' },
])
})
expect(results).toHaveLength(2)
expect(results[0].isAuthorized).toBe(true)
expect(results[1].isAuthorized).toBe(true)
})
})

describe('hook return shape', () => {
it('returns all expected properties', () => {
const store = createStore()
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })

expect(typeof result.current.authorize).toBe('function')
expect(typeof result.current.authorizeHelper).toBe('function')
expect(typeof result.current.hasCedarReadPermission).toBe('function')
expect(typeof result.current.hasCedarWritePermission).toBe('function')
expect(typeof result.current.hasCedarDeletePermission).toBe('function')
expect(typeof result.current.isLoading).toBe('boolean')
expect(result.current.error).toBeNull()
})

it('reflects loading state from store', () => {
const store = createStore({ cedarState: { loading: true } })
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.isLoading).toBe(true)
})

it('reflects error state from store', () => {
const store = createStore({ cedarState: { error: 'test error' } })
const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) })
expect(result.current.error).toBe('test error')
})
})
})
Loading
Loading