@jmruthers/pace-core 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -58
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +441 -0
  174. package/src/rbac/hooks/index.ts +21 -0
  175. package/src/rbac/hooks/useCan.test.ts +461 -0
  176. package/src/rbac/hooks/usePermissions.test.ts +359 -0
  177. package/src/rbac/hooks/usePermissions.ts +567 -0
  178. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  179. package/src/rbac/hooks/useRBAC.test.ts +503 -0
  180. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  181. package/src/rbac/index.ts +5 -10
  182. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  183. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  184. package/src/rbac/providers/index.ts +11 -0
  185. package/src/styles/core.css +0 -58
  186. package/src/utils/formatDate.test.ts +241 -0
  187. package/dist/chunk-AUE24LVR.js +0 -268
  188. package/dist/chunk-AUE24LVR.js.map +0 -1
  189. package/dist/chunk-COBPIXXQ.js +0 -379
  190. package/dist/chunk-COBPIXXQ.js.map +0 -1
  191. package/dist/chunk-OEGRKULD.js.map +0 -1
  192. package/dist/chunk-OYRY44Q2.js +0 -62
  193. package/dist/chunk-OYRY44Q2.js.map +0 -1
  194. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  195. package/dist/chunk-TGDCLPP2.js.map +0 -1
  196. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  197. package/src/components/RBAC/RBACGuard.tsx +0 -143
  198. package/src/components/RBAC/RBACProvider.tsx +0 -186
  199. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  200. package/src/components/RBAC/index.ts +0 -23
  201. package/src/rbac/hooks.ts +0 -570
  202. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  203. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  204. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  205. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  206. /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
@@ -0,0 +1,528 @@
1
+ /**
2
+ * @file useOrganisationPermissions Hook Tests
3
+ * @description Comprehensive tests for the useOrganisationPermissions hook
4
+ */
5
+
6
+ import { renderHook, act } from '@testing-library/react';
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { useOrganisationPermissions } from './useOrganisationPermissions';
9
+ import { useOrganisations } from '../providers/OrganisationProvider';
10
+ import type { Organisation, OrganisationRole, OrganisationPermission } from '../types/organisation';
11
+
12
+ // Mock the OrganisationProvider
13
+ vi.mock('../providers/OrganisationProvider', () => ({
14
+ useOrganisations: vi.fn()
15
+ }));
16
+
17
+ const mockUseOrganisations = vi.mocked(useOrganisations);
18
+
19
+ describe('useOrganisationPermissions Hook', () => {
20
+ const mockOrganisation: Organisation = {
21
+ id: 'org-123',
22
+ name: 'test-org',
23
+ display_name: 'Test Organisation',
24
+ subscription_tier: 'standard',
25
+ settings: {},
26
+ is_active: true,
27
+ created_at: '2024-01-01T00:00:00Z',
28
+ updated_at: '2024-01-01T00:00:00Z'
29
+ };
30
+
31
+ const mockOrganisationContext = {
32
+ selectedOrganisation: mockOrganisation,
33
+ organisations: [mockOrganisation],
34
+ userMemberships: [],
35
+ isLoading: false,
36
+ error: null,
37
+ hasValidOrganisationContext: true,
38
+ setSelectedOrganisation: vi.fn(),
39
+ switchOrganisation: vi.fn(),
40
+ getUserRole: vi.fn(),
41
+ validateOrganisationAccess: vi.fn(),
42
+ refreshOrganisations: vi.fn(),
43
+ ensureOrganisationContext: vi.fn(),
44
+ isOrganisationSecure: vi.fn(),
45
+ getPrimaryOrganisation: vi.fn()
46
+ };
47
+
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ mockUseOrganisations.mockReturnValue(mockOrganisationContext);
51
+ });
52
+
53
+ describe('Initialization', () => {
54
+ it('initializes with no access when no organisation context', () => {
55
+ mockUseOrganisations.mockReturnValue({
56
+ ...mockOrganisationContext,
57
+ selectedOrganisation: null,
58
+ hasValidOrganisationContext: false
59
+ });
60
+
61
+ const { result } = renderHook(() => useOrganisationPermissions());
62
+
63
+ expect(result.current.userRole).toBe('no_access');
64
+ expect(result.current.hasOrganisationAccess).toBe(false);
65
+ expect(result.current.isOrgAdmin).toBe(false);
66
+ expect(result.current.isSuperAdmin).toBe(false);
67
+ expect(result.current.canModerate).toBe(false);
68
+ expect(result.current.canManageMembers).toBe(false);
69
+ expect(result.current.canManageSettings).toBe(false);
70
+ expect(result.current.canManageEvents).toBe(false);
71
+ expect(result.current.hasAdminPrivileges).toBe(false);
72
+ });
73
+
74
+ it('initializes with organisation context', () => {
75
+ mockUseOrganisations.mockReturnValue({
76
+ ...mockOrganisationContext,
77
+ getUserRole: vi.fn().mockReturnValue('member'),
78
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
79
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
80
+ });
81
+
82
+ const { result } = renderHook(() => useOrganisationPermissions());
83
+
84
+ expect(result.current.organisationId).toBe('org-123');
85
+ expect(result.current.hasOrganisationAccess).toBe(true);
86
+ });
87
+
88
+ it('initializes with specific organisation ID', () => {
89
+ mockUseOrganisations.mockReturnValue({
90
+ ...mockOrganisationContext,
91
+ getUserRole: vi.fn().mockReturnValue('member'),
92
+ validateOrganisationAccess: vi.fn().mockReturnValue(true)
93
+ });
94
+
95
+ const { result } = renderHook(() => useOrganisationPermissions('org-456'));
96
+
97
+ expect(result.current.organisationId).toBe('org-456');
98
+ });
99
+ });
100
+
101
+ describe('Role-based Permissions', () => {
102
+ it('handles supporter role correctly', () => {
103
+ mockUseOrganisations.mockReturnValue({
104
+ ...mockOrganisationContext,
105
+ getUserRole: vi.fn().mockReturnValue('supporter'),
106
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
107
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
108
+ });
109
+
110
+ const { result } = renderHook(() => useOrganisationPermissions());
111
+
112
+ expect(result.current.userRole).toBe('supporter');
113
+ expect(result.current.isOrgAdmin).toBe(false);
114
+ expect(result.current.isSuperAdmin).toBe(false);
115
+ expect(result.current.canModerate).toBe(false);
116
+ expect(result.current.canManageMembers).toBe(false);
117
+ expect(result.current.canManageSettings).toBe(false);
118
+ expect(result.current.canManageEvents).toBe(false);
119
+ expect(result.current.hasAdminPrivileges).toBe(false);
120
+ });
121
+
122
+ it('handles member role correctly', () => {
123
+ mockUseOrganisations.mockReturnValue({
124
+ ...mockOrganisationContext,
125
+ getUserRole: vi.fn().mockReturnValue('member'),
126
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
127
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
128
+ });
129
+
130
+ const { result } = renderHook(() => useOrganisationPermissions());
131
+
132
+ expect(result.current.userRole).toBe('member');
133
+ expect(result.current.isOrgAdmin).toBe(false);
134
+ expect(result.current.isSuperAdmin).toBe(false);
135
+ expect(result.current.canModerate).toBe(false);
136
+ expect(result.current.canManageMembers).toBe(false);
137
+ expect(result.current.canManageSettings).toBe(false);
138
+ expect(result.current.canManageEvents).toBe(false);
139
+ expect(result.current.hasAdminPrivileges).toBe(false);
140
+ });
141
+
142
+ it('handles leader role correctly', () => {
143
+ mockUseOrganisations.mockReturnValue({
144
+ ...mockOrganisationContext,
145
+ getUserRole: vi.fn().mockReturnValue('leader'),
146
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
147
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
148
+ });
149
+
150
+ const { result } = renderHook(() => useOrganisationPermissions());
151
+
152
+ expect(result.current.userRole).toBe('leader');
153
+ expect(result.current.isOrgAdmin).toBe(false);
154
+ expect(result.current.isSuperAdmin).toBe(false);
155
+ expect(result.current.canModerate).toBe(true);
156
+ expect(result.current.canManageMembers).toBe(false);
157
+ expect(result.current.canManageSettings).toBe(false);
158
+ expect(result.current.canManageEvents).toBe(true);
159
+ expect(result.current.hasAdminPrivileges).toBe(false);
160
+ });
161
+
162
+ it('handles org_admin role correctly', () => {
163
+ mockUseOrganisations.mockReturnValue({
164
+ ...mockOrganisationContext,
165
+ getUserRole: vi.fn().mockReturnValue('org_admin'),
166
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
167
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
168
+ });
169
+
170
+ const { result } = renderHook(() => useOrganisationPermissions());
171
+
172
+ expect(result.current.userRole).toBe('org_admin');
173
+ expect(result.current.isOrgAdmin).toBe(true);
174
+ expect(result.current.isSuperAdmin).toBe(true); // org_admin acts as super admin within org
175
+ expect(result.current.canModerate).toBe(true);
176
+ expect(result.current.canManageMembers).toBe(true);
177
+ expect(result.current.canManageSettings).toBe(true);
178
+ expect(result.current.canManageEvents).toBe(true);
179
+ expect(result.current.hasAdminPrivileges).toBe(true);
180
+ });
181
+
182
+ it('handles no_access role correctly', () => {
183
+ mockUseOrganisations.mockReturnValue({
184
+ ...mockOrganisationContext,
185
+ getUserRole: vi.fn().mockReturnValue('no_access'),
186
+ validateOrganisationAccess: vi.fn().mockReturnValue(false),
187
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
188
+ });
189
+
190
+ const { result } = renderHook(() => useOrganisationPermissions());
191
+
192
+ expect(result.current.userRole).toBe('no_access');
193
+ expect(result.current.hasOrganisationAccess).toBe(false);
194
+ expect(result.current.isOrgAdmin).toBe(false);
195
+ expect(result.current.isSuperAdmin).toBe(false);
196
+ expect(result.current.canModerate).toBe(false);
197
+ expect(result.current.canManageMembers).toBe(false);
198
+ expect(result.current.canManageSettings).toBe(false);
199
+ expect(result.current.canManageEvents).toBe(false);
200
+ expect(result.current.hasAdminPrivileges).toBe(false);
201
+ });
202
+ });
203
+
204
+ describe('Permission Checking', () => {
205
+ it('hasPermission returns false for no access', () => {
206
+ mockUseOrganisations.mockReturnValue({
207
+ ...mockOrganisationContext,
208
+ getUserRole: vi.fn().mockReturnValue('no_access'),
209
+ validateOrganisationAccess: vi.fn().mockReturnValue(false)
210
+ });
211
+
212
+ const { result } = renderHook(() => useOrganisationPermissions());
213
+
214
+ expect(result.current.hasPermission('view_basic')).toBe(false);
215
+ expect(result.current.hasPermission('manage_members')).toBe(false);
216
+ });
217
+
218
+ it('hasPermission returns correct permissions for supporter', () => {
219
+ mockUseOrganisations.mockReturnValue({
220
+ ...mockOrganisationContext,
221
+ getUserRole: vi.fn().mockReturnValue('supporter'),
222
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
223
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
224
+ });
225
+
226
+ const { result } = renderHook(() => useOrganisationPermissions());
227
+
228
+ expect(result.current.hasPermission('view_basic')).toBe(true);
229
+ expect(result.current.hasPermission('view_details')).toBe(false);
230
+ expect(result.current.hasPermission('moderate_content')).toBe(false);
231
+ expect(result.current.hasPermission('manage_events')).toBe(false);
232
+ expect(result.current.hasPermission('manage_members')).toBe(false);
233
+ expect(result.current.hasPermission('manage_settings')).toBe(false);
234
+ });
235
+
236
+ it('hasPermission returns correct permissions for member', () => {
237
+ mockUseOrganisations.mockReturnValue({
238
+ ...mockOrganisationContext,
239
+ getUserRole: vi.fn().mockReturnValue('member'),
240
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
241
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
242
+ });
243
+
244
+ const { result } = renderHook(() => useOrganisationPermissions());
245
+
246
+ expect(result.current.hasPermission('view_basic')).toBe(true);
247
+ expect(result.current.hasPermission('view_details')).toBe(true);
248
+ expect(result.current.hasPermission('moderate_content')).toBe(false);
249
+ expect(result.current.hasPermission('manage_events')).toBe(false);
250
+ expect(result.current.hasPermission('manage_members')).toBe(false);
251
+ expect(result.current.hasPermission('manage_settings')).toBe(false);
252
+ });
253
+
254
+ it('hasPermission returns correct permissions for leader', () => {
255
+ mockUseOrganisations.mockReturnValue({
256
+ ...mockOrganisationContext,
257
+ getUserRole: vi.fn().mockReturnValue('leader'),
258
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
259
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
260
+ });
261
+
262
+ const { result } = renderHook(() => useOrganisationPermissions());
263
+
264
+ expect(result.current.hasPermission('view_basic')).toBe(true);
265
+ expect(result.current.hasPermission('view_details')).toBe(true);
266
+ expect(result.current.hasPermission('moderate_content')).toBe(true);
267
+ expect(result.current.hasPermission('manage_events')).toBe(true);
268
+ expect(result.current.hasPermission('manage_members')).toBe(false);
269
+ expect(result.current.hasPermission('manage_settings')).toBe(false);
270
+ });
271
+
272
+ it('hasPermission returns correct permissions for org_admin', () => {
273
+ mockUseOrganisations.mockReturnValue({
274
+ ...mockOrganisationContext,
275
+ getUserRole: vi.fn().mockReturnValue('org_admin'),
276
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
277
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
278
+ });
279
+
280
+ const { result } = renderHook(() => useOrganisationPermissions());
281
+
282
+ expect(result.current.hasPermission('view_basic')).toBe(true);
283
+ expect(result.current.hasPermission('view_details')).toBe(true);
284
+ expect(result.current.hasPermission('moderate_content')).toBe(true);
285
+ expect(result.current.hasPermission('manage_events')).toBe(true);
286
+ expect(result.current.hasPermission('manage_members')).toBe(true);
287
+ expect(result.current.hasPermission('manage_settings')).toBe(true);
288
+ });
289
+
290
+ it('hasPermission returns true for wildcard permission for org_admin', () => {
291
+ mockUseOrganisations.mockReturnValue({
292
+ ...mockOrganisationContext,
293
+ getUserRole: vi.fn().mockReturnValue('org_admin'),
294
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
295
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
296
+ });
297
+
298
+ const { result } = renderHook(() => useOrganisationPermissions());
299
+
300
+ expect(result.current.hasPermission('*')).toBe(true);
301
+ });
302
+ });
303
+
304
+ describe('getAllPermissions', () => {
305
+ it('returns empty array for no access', () => {
306
+ mockUseOrganisations.mockReturnValue({
307
+ ...mockOrganisationContext,
308
+ getUserRole: vi.fn().mockReturnValue('no_access'),
309
+ validateOrganisationAccess: vi.fn().mockReturnValue(false)
310
+ });
311
+
312
+ const { result } = renderHook(() => useOrganisationPermissions());
313
+
314
+ expect(result.current.getAllPermissions()).toEqual([]);
315
+ });
316
+
317
+ it('returns correct permissions for supporter', () => {
318
+ mockUseOrganisations.mockReturnValue({
319
+ ...mockOrganisationContext,
320
+ getUserRole: vi.fn().mockReturnValue('supporter'),
321
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
322
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
323
+ });
324
+
325
+ const { result } = renderHook(() => useOrganisationPermissions());
326
+
327
+ expect(result.current.getAllPermissions()).toEqual(['view_basic']);
328
+ });
329
+
330
+ it('returns correct permissions for member', () => {
331
+ mockUseOrganisations.mockReturnValue({
332
+ ...mockOrganisationContext,
333
+ getUserRole: vi.fn().mockReturnValue('member'),
334
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
335
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
336
+ });
337
+
338
+ const { result } = renderHook(() => useOrganisationPermissions());
339
+
340
+ expect(result.current.getAllPermissions()).toEqual(['view_basic', 'view_details']);
341
+ });
342
+
343
+ it('returns correct permissions for leader', () => {
344
+ mockUseOrganisations.mockReturnValue({
345
+ ...mockOrganisationContext,
346
+ getUserRole: vi.fn().mockReturnValue('leader'),
347
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
348
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
349
+ });
350
+
351
+ const { result } = renderHook(() => useOrganisationPermissions());
352
+
353
+ expect(result.current.getAllPermissions()).toEqual([
354
+ 'view_basic',
355
+ 'view_details',
356
+ 'moderate_content',
357
+ 'manage_events'
358
+ ]);
359
+ });
360
+
361
+ it('returns correct permissions for org_admin', () => {
362
+ mockUseOrganisations.mockReturnValue({
363
+ ...mockOrganisationContext,
364
+ getUserRole: vi.fn().mockReturnValue('org_admin'),
365
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
366
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
367
+ });
368
+
369
+ const { result } = renderHook(() => useOrganisationPermissions());
370
+
371
+ expect(result.current.getAllPermissions()).toEqual([
372
+ 'view_basic',
373
+ 'view_details',
374
+ 'moderate_content',
375
+ 'manage_events',
376
+ 'manage_members',
377
+ 'manage_settings'
378
+ ]);
379
+ });
380
+ });
381
+
382
+ describe('Organisation Access Validation', () => {
383
+ it('returns false when organisation access is invalid', () => {
384
+ mockUseOrganisations.mockReturnValue({
385
+ ...mockOrganisationContext,
386
+ getUserRole: vi.fn().mockReturnValue('member'),
387
+ validateOrganisationAccess: vi.fn().mockReturnValue(false)
388
+ });
389
+
390
+ const { result } = renderHook(() => useOrganisationPermissions());
391
+
392
+ expect(result.current.hasOrganisationAccess).toBe(false);
393
+ });
394
+
395
+ it('returns true when organisation access is valid', () => {
396
+ mockUseOrganisations.mockReturnValue({
397
+ ...mockOrganisationContext,
398
+ getUserRole: vi.fn().mockReturnValue('member'),
399
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
400
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
401
+ });
402
+
403
+ const { result } = renderHook(() => useOrganisationPermissions());
404
+
405
+ expect(result.current.hasOrganisationAccess).toBe(true);
406
+ });
407
+ });
408
+
409
+ describe('Edge Cases', () => {
410
+ it('handles missing organisation context gracefully', () => {
411
+ mockUseOrganisations.mockReturnValue({
412
+ ...mockOrganisationContext,
413
+ selectedOrganisation: null,
414
+ hasValidOrganisationContext: false,
415
+ ensureOrganisationContext: vi.fn().mockImplementation(() => {
416
+ throw new Error('No organisation context');
417
+ })
418
+ });
419
+
420
+ const { result } = renderHook(() => useOrganisationPermissions());
421
+
422
+ expect(result.current.organisationId).toBe('');
423
+ expect(result.current.userRole).toBe('no_access');
424
+ expect(result.current.hasOrganisationAccess).toBe(false);
425
+ });
426
+
427
+ it('handles invalid organisation ID', () => {
428
+ mockUseOrganisations.mockReturnValue({
429
+ ...mockOrganisationContext,
430
+ getUserRole: vi.fn().mockReturnValue('no_access'),
431
+ validateOrganisationAccess: vi.fn().mockReturnValue(false)
432
+ });
433
+
434
+ const { result } = renderHook(() => useOrganisationPermissions('invalid-org'));
435
+
436
+ expect(result.current.organisationId).toBe('invalid-org');
437
+ expect(result.current.userRole).toBe('no_access');
438
+ expect(result.current.hasOrganisationAccess).toBe(false);
439
+ });
440
+
441
+ it('handles undefined organisation ID parameter', () => {
442
+ mockUseOrganisations.mockReturnValue({
443
+ ...mockOrganisationContext,
444
+ getUserRole: vi.fn().mockReturnValue('member'),
445
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
446
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
447
+ });
448
+
449
+ const { result } = renderHook(() => useOrganisationPermissions(undefined));
450
+
451
+ expect(result.current.organisationId).toBe('org-123');
452
+ });
453
+ });
454
+
455
+ describe('Memoization', () => {
456
+ it('maintains stable references when dependencies do not change', () => {
457
+ mockUseOrganisations.mockReturnValue({
458
+ ...mockOrganisationContext,
459
+ getUserRole: vi.fn().mockReturnValue('member'),
460
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
461
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
462
+ });
463
+
464
+ const { result, rerender } = renderHook(() => useOrganisationPermissions());
465
+
466
+ const firstResult = result.current;
467
+ rerender();
468
+ const secondResult = result.current;
469
+
470
+ expect(firstResult.hasPermission).toBe(secondResult.hasPermission);
471
+ expect(firstResult.getAllPermissions).toBe(secondResult.getAllPermissions);
472
+ });
473
+
474
+ it('updates when organisation context changes', () => {
475
+ const { result, rerender } = renderHook(() => useOrganisationPermissions());
476
+
477
+ // Initial state
478
+ mockUseOrganisations.mockReturnValue({
479
+ ...mockOrganisationContext,
480
+ getUserRole: vi.fn().mockReturnValue('member'),
481
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
482
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
483
+ });
484
+
485
+ rerender();
486
+
487
+ expect(result.current.userRole).toBe('member');
488
+
489
+ // Change role
490
+ mockUseOrganisations.mockReturnValue({
491
+ ...mockOrganisationContext,
492
+ getUserRole: vi.fn().mockReturnValue('leader'),
493
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
494
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
495
+ });
496
+
497
+ rerender();
498
+
499
+ expect(result.current.userRole).toBe('leader');
500
+ });
501
+ });
502
+
503
+ describe('Type Safety', () => {
504
+ it('returns correct types for all properties', () => {
505
+ mockUseOrganisations.mockReturnValue({
506
+ ...mockOrganisationContext,
507
+ getUserRole: vi.fn().mockReturnValue('member'),
508
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
509
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation)
510
+ });
511
+
512
+ const { result } = renderHook(() => useOrganisationPermissions());
513
+
514
+ expect(typeof result.current.userRole).toBe('string');
515
+ expect(typeof result.current.organisationId).toBe('string');
516
+ expect(typeof result.current.hasOrganisationAccess).toBe('boolean');
517
+ expect(typeof result.current.isOrgAdmin).toBe('boolean');
518
+ expect(typeof result.current.isSuperAdmin).toBe('boolean');
519
+ expect(typeof result.current.canModerate).toBe('boolean');
520
+ expect(typeof result.current.canManageMembers).toBe('boolean');
521
+ expect(typeof result.current.canManageSettings).toBe('boolean');
522
+ expect(typeof result.current.canManageEvents).toBe('boolean');
523
+ expect(typeof result.current.hasAdminPrivileges).toBe('boolean');
524
+ expect(typeof result.current.hasPermission).toBe('function');
525
+ expect(typeof result.current.getAllPermissions).toBe('function');
526
+ });
527
+ });
528
+ });