Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 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
46 changes: 46 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,46 @@
import { cedarlingClient } from '@/cedarling/client/CedarlingClient'
import type { TokenAuthorizationRequest } from '@/cedarling'

// 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({})
await expect(cedarlingClient.initialize({})).resolves.toBeUndefined()
})
})

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,59 @@
import { CEDAR_RESOURCE_SCOPES, CEDARLING_CONSTANTS } from '@/cedarling/constants/resourceScopes'
import { ADMIN_UI_RESOURCES } from '@/cedarling/utility'
import type { AdminUiFeatureResource } from '@/cedarling'

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.some((p) => p.includes('read') || p.includes('lock'))).toBe(true)
})

it('SMTP has read, write, and delete scopes', () => {
const smtpScopes = CEDAR_RESOURCE_SCOPES.SMTP
expect(smtpScopes).toHaveLength(3)
})

it('Dashboard has stat read 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)
})
})
223 changes: 223 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,223 @@
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('returns empty array for undefined-like input', 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([])
})
})

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