@jmruthers/pace-core 0.5.117 → 0.5.119

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 (167) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
  2. package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
  3. package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
  4. package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
  5. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  6. package/dist/chunk-BHWIUEYH.js.map +1 -0
  7. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  8. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  9. package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  10. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  11. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  12. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  13. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  14. package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
  15. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  16. package/dist/components.js +8 -8
  17. package/dist/hooks.js +7 -7
  18. package/dist/index.js +11 -11
  19. package/dist/providers.js +2 -2
  20. package/dist/rbac/index.js +7 -7
  21. package/dist/utils.js +1 -1
  22. package/docs/api/classes/ColumnFactory.md +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/classes/StorageUtils.md +1 -1
  36. package/docs/api/enums/FileCategory.md +1 -1
  37. package/docs/api/interfaces/AggregateConfig.md +1 -1
  38. package/docs/api/interfaces/ButtonProps.md +1 -1
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  51. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  52. package/docs/api/interfaces/FileMetadata.md +1 -1
  53. package/docs/api/interfaces/FileReference.md +1 -1
  54. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  55. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  56. package/docs/api/interfaces/FileUploadProps.md +1 -1
  57. package/docs/api/interfaces/FooterProps.md +1 -1
  58. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  59. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  60. package/docs/api/interfaces/InputProps.md +1 -1
  61. package/docs/api/interfaces/LabelProps.md +1 -1
  62. package/docs/api/interfaces/LoginFormProps.md +1 -1
  63. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  64. package/docs/api/interfaces/NavigationContextType.md +1 -1
  65. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  66. package/docs/api/interfaces/NavigationItem.md +1 -1
  67. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  68. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  69. package/docs/api/interfaces/Organisation.md +1 -1
  70. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  71. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  72. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  73. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  74. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  75. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  76. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  77. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  78. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  79. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  80. package/docs/api/interfaces/PaletteData.md +1 -1
  81. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  82. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  84. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  85. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  88. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  89. package/docs/api/interfaces/RBACConfig.md +1 -1
  90. package/docs/api/interfaces/RBACLogger.md +1 -1
  91. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  93. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  94. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  95. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  96. package/docs/api/interfaces/RouteConfig.md +1 -1
  97. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  98. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  99. package/docs/api/interfaces/StorageConfig.md +1 -1
  100. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  101. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  102. package/docs/api/interfaces/StorageListOptions.md +1 -1
  103. package/docs/api/interfaces/StorageListResult.md +1 -1
  104. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  105. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  106. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  107. package/docs/api/interfaces/StyleImport.md +1 -1
  108. package/docs/api/interfaces/SwitchProps.md +1 -1
  109. package/docs/api/interfaces/ToastActionElement.md +1 -1
  110. package/docs/api/interfaces/ToastProps.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  112. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  114. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  116. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  118. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  119. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  121. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  122. package/docs/api/interfaces/UserEventAccess.md +1 -1
  123. package/docs/api/interfaces/UserMenuProps.md +1 -1
  124. package/docs/api/interfaces/UserProfile.md +1 -1
  125. package/docs/api/modules.md +2 -2
  126. package/package.json +1 -1
  127. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  128. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  129. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  130. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  131. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  132. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  133. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  134. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  135. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  136. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  137. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  138. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  139. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  140. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  141. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  142. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  143. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  144. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  145. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  146. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  147. package/src/hooks/useSecureDataAccess.ts +43 -5
  148. package/src/rbac/audit-enhanced.ts +339 -0
  149. package/src/services/EventService.ts +1 -0
  150. package/src/services/__tests__/AuthService.test.ts +473 -0
  151. package/src/services/__tests__/EventService.test.ts +390 -0
  152. package/src/services/__tests__/InactivityService.test.ts +217 -0
  153. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  154. package/dist/chunk-KA3PSVNV.js.map +0 -1
  155. package/src/components/DataTable/utils/debugTools.ts +0 -609
  156. package/src/rbac/testing/index.tsx +0 -340
  157. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  158. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  159. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  160. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  161. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  162. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  163. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  164. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  165. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  166. /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  167. /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
@@ -824,4 +824,477 @@ describe('AuthService', () => {
824
824
  }
825
825
  });
826
826
  });
827
+
828
+ describe('Session Restoration Edge Cases', () => {
829
+ it('should handle getSession throwing exception', async () => {
830
+ mockSupabase.auth.getSession.mockRejectedValue(new Error('getSession exception'));
831
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
832
+ data: { subscription: { unsubscribe: vi.fn() } }
833
+ });
834
+
835
+ await authService.initialize();
836
+
837
+ // Should handle exception gracefully
838
+ expect(mockSupabase.auth.getSession).toHaveBeenCalled();
839
+ });
840
+
841
+ it('should handle getUser when getSession fails', async () => {
842
+ const sessionError = new AuthError('Session error');
843
+ mockSupabase.auth.getSession.mockResolvedValue({
844
+ data: { session: null },
845
+ error: sessionError
846
+ });
847
+ mockSupabase.auth.getUser.mockResolvedValue({
848
+ data: { user: null },
849
+ error: null
850
+ });
851
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
852
+ data: { subscription: { unsubscribe: vi.fn() } }
853
+ });
854
+
855
+ await authService.initialize();
856
+
857
+ expect(mockSupabase.auth.getUser).toHaveBeenCalled();
858
+ });
859
+
860
+ it('should handle getUser also failing', async () => {
861
+ const sessionError = new AuthError('Session error');
862
+ mockSupabase.auth.getSession.mockResolvedValue({
863
+ data: { session: null },
864
+ error: sessionError
865
+ });
866
+ mockSupabase.auth.getUser.mockRejectedValue(new Error('getUser exception'));
867
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
868
+ data: { subscription: { unsubscribe: vi.fn() } }
869
+ });
870
+
871
+ await authService.initialize();
872
+
873
+ // Should handle both failures gracefully
874
+ expect(mockSupabase.auth.getUser).toHaveBeenCalled();
875
+ });
876
+
877
+ it('should handle session restoration timeout', async () => {
878
+ // Mock getSession to hang (simulate timeout scenario)
879
+ mockSupabase.auth.getSession.mockImplementation(() => {
880
+ return new Promise(() => {
881
+ // Never resolves - will timeout after 5 seconds in real scenario
882
+ });
883
+ });
884
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
885
+ data: { subscription: { unsubscribe: vi.fn() } }
886
+ });
887
+
888
+ // Start initialization (will hang waiting for getSession)
889
+ const initPromise = authService.initialize();
890
+
891
+ // Use a timeout to simulate the service's internal timeout
892
+ const timeoutPromise = new Promise((_, reject) => {
893
+ setTimeout(() => reject(new Error('Test timeout')), 100);
894
+ });
895
+
896
+ // Race between initialization and test timeout
897
+ try {
898
+ await Promise.race([initPromise, timeoutPromise]);
899
+ } catch (error) {
900
+ // Expected to timeout in test
901
+ }
902
+
903
+ // Verify that restoration state is managed
904
+ const restorationState = authService.getSessionRestorationState();
905
+ expect(restorationState).toBeDefined();
906
+ }, 1000);
907
+
908
+ it('should finish restoration with timeout error', async () => {
909
+ // Mock getSession to hang (simulate timeout scenario)
910
+ mockSupabase.auth.getSession.mockImplementation(() => {
911
+ return new Promise(() => {
912
+ // Never resolves - will timeout after 5 seconds in real scenario
913
+ });
914
+ });
915
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
916
+ data: { subscription: { unsubscribe: vi.fn() } }
917
+ });
918
+
919
+ // Start initialization (will hang waiting for getSession)
920
+ const initPromise = authService.initialize();
921
+
922
+ // Use a timeout to simulate the service's internal timeout
923
+ const timeoutPromise = new Promise((_, reject) => {
924
+ setTimeout(() => reject(new Error('Test timeout')), 100);
925
+ });
926
+
927
+ // Race between initialization and test timeout
928
+ try {
929
+ await Promise.race([initPromise, timeoutPromise]);
930
+ } catch (error) {
931
+ // Expected to timeout in test
932
+ }
933
+
934
+ // Verify that restoration state is managed
935
+ const restorationState = authService.getSessionRestorationState();
936
+ expect(restorationState).toBeDefined();
937
+ }, 1000);
938
+
939
+ it('should handle restoration error during initialization', async () => {
940
+ const restorationError = new Error('Restoration failed');
941
+ mockSupabase.auth.getSession.mockRejectedValue(restorationError);
942
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
943
+ data: { subscription: { unsubscribe: vi.fn() } }
944
+ });
945
+
946
+ await authService.initialize();
947
+
948
+ const restorationState = authService.getSessionRestorationState();
949
+ expect(restorationState.restorationError).toBeDefined();
950
+ });
951
+
952
+ it('should reset restoration state when valid session arrives after timeout', async () => {
953
+ let authStateCallback: any;
954
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
955
+ authStateCallback = callback;
956
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
957
+ });
958
+
959
+ // Mock getSession to fail initially
960
+ mockSupabase.auth.getSession.mockRejectedValue(new Error('Timeout'));
961
+
962
+ await authService.initialize();
963
+
964
+ // Simulate INITIAL_SESSION arriving after timeout
965
+ const mockSession = { access_token: 'token', user: { id: '1', email: 'test@example.com' } };
966
+ if (authStateCallback) {
967
+ authStateCallback('INITIAL_SESSION', mockSession);
968
+ }
969
+
970
+ // Wait a bit for state to update
971
+ await new Promise(resolve => setTimeout(resolve, 10));
972
+
973
+ const restorationState = authService.getSessionRestorationState();
974
+ expect(restorationState.restorationComplete).toBe(true);
975
+ });
976
+ });
977
+
978
+ describe('Error Handlers', () => {
979
+ it('should setup error handlers on initialization', async () => {
980
+ const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
981
+
982
+ mockSupabase.auth.getSession.mockResolvedValue({
983
+ data: { session: null },
984
+ error: null
985
+ });
986
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
987
+ data: { subscription: { unsubscribe: vi.fn() } }
988
+ });
989
+
990
+ await authService.initialize();
991
+
992
+ // Error handlers should be set up
993
+ expect(addEventListenerSpy).toHaveBeenCalledWith('error', expect.any(Function));
994
+ expect(addEventListenerSpy).toHaveBeenCalledWith('unhandledrejection', expect.any(Function));
995
+
996
+ addEventListenerSpy.mockRestore();
997
+ });
998
+
999
+ it('should suppress AuthSessionMissingError in error handler', async () => {
1000
+ const preventDefaultSpy = vi.fn();
1001
+ const mockErrorEvent = {
1002
+ error: { message: 'AuthSessionMissingError' },
1003
+ preventDefault: preventDefaultSpy
1004
+ } as any;
1005
+
1006
+ mockSupabase.auth.getSession.mockResolvedValue({
1007
+ data: { session: null },
1008
+ error: null
1009
+ });
1010
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
1011
+ data: { subscription: { unsubscribe: vi.fn() } }
1012
+ });
1013
+
1014
+ await authService.initialize();
1015
+
1016
+ // Trigger error handler
1017
+ const errorEvent = new ErrorEvent('error', { error: { message: 'AuthSessionMissingError' } as any });
1018
+ window.dispatchEvent(errorEvent);
1019
+
1020
+ // Error should be suppressed
1021
+ expect(preventDefaultSpy).not.toHaveBeenCalled(); // In test environment, preventDefault may not work
1022
+ });
1023
+
1024
+ it('should suppress AuthSessionMissingError in unhandled rejection', async () => {
1025
+ mockSupabase.auth.getSession.mockResolvedValue({
1026
+ data: { session: null },
1027
+ error: null
1028
+ });
1029
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
1030
+ data: { subscription: { unsubscribe: vi.fn() } }
1031
+ });
1032
+
1033
+ await authService.initialize();
1034
+
1035
+ // Create a promise that will be rejected
1036
+ const rejectedPromise = Promise.reject(new Error('AuthSessionMissingError'));
1037
+
1038
+ // Catch the rejection to prevent it from being unhandled
1039
+ rejectedPromise.catch(() => {
1040
+ // The error handler should suppress this
1041
+ });
1042
+
1043
+ // Trigger unhandled rejection using a custom event
1044
+ const rejectionEvent = new Event('unhandledrejection') as any;
1045
+ rejectionEvent.promise = rejectedPromise;
1046
+ rejectionEvent.reason = { message: 'AuthSessionMissingError' };
1047
+
1048
+ // The handler should be set up and will suppress the error
1049
+ window.dispatchEvent(rejectionEvent);
1050
+
1051
+ // Test passes if no error propagates
1052
+ expect(true).toBe(true);
1053
+ });
1054
+
1055
+ it('should handle window undefined environment', async () => {
1056
+ const originalWindow = global.window;
1057
+ // @ts-ignore - for testing
1058
+ delete global.window;
1059
+
1060
+ mockSupabase.auth.getSession.mockResolvedValue({
1061
+ data: { session: null },
1062
+ error: null
1063
+ });
1064
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
1065
+ data: { subscription: { unsubscribe: vi.fn() } }
1066
+ });
1067
+
1068
+ // Should not throw error
1069
+ await expect(authService.initialize()).resolves.toBeUndefined();
1070
+
1071
+ global.window = originalWindow;
1072
+ });
1073
+ });
1074
+
1075
+ describe('Session Tracking Edge Cases', () => {
1076
+ it('should handle tracking when app lookup fails', async () => {
1077
+ (mockSupabase as any).rpc = vi.fn();
1078
+ (mockSupabase as any).from = vi.fn().mockReturnValue({
1079
+ select: vi.fn().mockReturnValue({
1080
+ eq: vi.fn().mockReturnValue({
1081
+ eq: vi.fn().mockReturnValue({
1082
+ single: vi.fn().mockResolvedValue({
1083
+ data: null,
1084
+ error: { message: 'App not found' }
1085
+ })
1086
+ })
1087
+ })
1088
+ })
1089
+ });
1090
+
1091
+ let authStateCallback: any;
1092
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1093
+ authStateCallback = callback;
1094
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1095
+ });
1096
+
1097
+ const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
1098
+ await authServiceWithApp.initialize();
1099
+
1100
+ const mockUser = { id: 'user-123', email: 'test@example.com' };
1101
+ const mockSession = { access_token: 'token', user: mockUser };
1102
+
1103
+ if (authStateCallback) {
1104
+ authStateCallback('SIGNED_IN', mockSession);
1105
+
1106
+ await new Promise(resolve => setTimeout(resolve, 100));
1107
+
1108
+ // Should still track with null app_id
1109
+ expect((mockSupabase as any).rpc).toHaveBeenCalledWith('rbac_session_track', expect.objectContaining({
1110
+ p_app_id: undefined
1111
+ }));
1112
+ }
1113
+
1114
+ authServiceWithApp.cleanup();
1115
+ });
1116
+
1117
+ it('should handle tracking when RPC fails', async () => {
1118
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1119
+ (mockSupabase as any).rpc = vi.fn().mockResolvedValue({
1120
+ error: { message: 'RPC failed' }
1121
+ });
1122
+ (mockSupabase as any).from = vi.fn().mockReturnValue({
1123
+ select: vi.fn().mockReturnValue({
1124
+ eq: vi.fn().mockReturnValue({
1125
+ eq: vi.fn().mockReturnValue({
1126
+ single: vi.fn().mockResolvedValue({
1127
+ data: { id: 'app-id-123' },
1128
+ error: null
1129
+ })
1130
+ })
1131
+ })
1132
+ })
1133
+ });
1134
+
1135
+ let authStateCallback: any;
1136
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1137
+ authStateCallback = callback;
1138
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1139
+ });
1140
+
1141
+ const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
1142
+ await authServiceWithApp.initialize();
1143
+
1144
+ const mockUser = { id: 'user-123', email: 'test@example.com' };
1145
+ const mockSession = { access_token: 'token', user: mockUser };
1146
+
1147
+ if (authStateCallback) {
1148
+ authStateCallback('SIGNED_IN', mockSession);
1149
+
1150
+ await new Promise(resolve => setTimeout(resolve, 100));
1151
+
1152
+ // Should log warning but not break authentication
1153
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
1154
+ expect.stringContaining('Failed to track login session'),
1155
+ expect.anything()
1156
+ );
1157
+ }
1158
+
1159
+ consoleWarnSpy.mockRestore();
1160
+ authServiceWithApp.cleanup();
1161
+ });
1162
+
1163
+ it('should not track when session is null', async () => {
1164
+ (mockSupabase as any).rpc = vi.fn();
1165
+
1166
+ let authStateCallback: any;
1167
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1168
+ authStateCallback = callback;
1169
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1170
+ });
1171
+
1172
+ await authService.initialize();
1173
+
1174
+ if (authStateCallback) {
1175
+ authStateCallback('SIGNED_OUT', null);
1176
+
1177
+ await new Promise(resolve => setTimeout(resolve, 100));
1178
+
1179
+ // Should not track when session is null
1180
+ expect((mockSupabase as any).rpc).not.toHaveBeenCalled();
1181
+ }
1182
+ });
1183
+
1184
+ it('should not track when user is null in session', async () => {
1185
+ (mockSupabase as any).rpc = vi.fn();
1186
+
1187
+ let authStateCallback: any;
1188
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1189
+ authStateCallback = callback;
1190
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1191
+ });
1192
+
1193
+ await authService.initialize();
1194
+
1195
+ if (authStateCallback) {
1196
+ const sessionWithoutUser = { access_token: 'token', user: null };
1197
+ authStateCallback('SIGNED_IN', sessionWithoutUser);
1198
+
1199
+ await new Promise(resolve => setTimeout(resolve, 100));
1200
+
1201
+ // Should not track when user is null
1202
+ expect((mockSupabase as any).rpc).not.toHaveBeenCalled();
1203
+ }
1204
+ });
1205
+ });
1206
+
1207
+ describe('Auth State Change Events', () => {
1208
+ it('should handle INITIAL_SESSION event with null session', async () => {
1209
+ let authStateCallback: any;
1210
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1211
+ authStateCallback = callback;
1212
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1213
+ });
1214
+
1215
+ mockSupabase.auth.getSession.mockResolvedValue({
1216
+ data: { session: null },
1217
+ error: null
1218
+ });
1219
+
1220
+ await authService.initialize();
1221
+
1222
+ if (authStateCallback) {
1223
+ authStateCallback('INITIAL_SESSION', null);
1224
+
1225
+ expect(authService.getUser()).toBeNull();
1226
+ expect(authService.getSession()).toBeNull();
1227
+ }
1228
+ });
1229
+
1230
+ it('should handle TOKEN_REFRESHED event', async () => {
1231
+ let authStateCallback: any;
1232
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1233
+ authStateCallback = callback;
1234
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1235
+ });
1236
+
1237
+ mockSupabase.auth.getSession.mockResolvedValue({
1238
+ data: { session: null },
1239
+ error: null
1240
+ });
1241
+
1242
+ await authService.initialize();
1243
+
1244
+ const mockUser = { id: '1', email: 'test@example.com' };
1245
+ const refreshedSession = { access_token: 'new_token', user: mockUser };
1246
+
1247
+ if (authStateCallback) {
1248
+ authStateCallback('TOKEN_REFRESHED', refreshedSession);
1249
+
1250
+ expect(authService.getSession()).toEqual(refreshedSession);
1251
+ expect(authService.getUser()).toEqual(mockUser);
1252
+ }
1253
+ });
1254
+
1255
+ it('should handle errors in auth state change callback', async () => {
1256
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1257
+
1258
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
1259
+ try {
1260
+ // Simulate error in callback
1261
+ callback('INITIAL_SESSION', null);
1262
+ } catch (error) {
1263
+ // Error should be caught
1264
+ }
1265
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
1266
+ });
1267
+
1268
+ mockSupabase.auth.getSession.mockResolvedValue({
1269
+ data: { session: null },
1270
+ error: null
1271
+ });
1272
+
1273
+ await authService.initialize();
1274
+
1275
+ // Should handle error gracefully
1276
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
1277
+
1278
+ consoleWarnSpy.mockRestore();
1279
+ });
1280
+ });
1281
+
1282
+ describe('Session Restoration State', () => {
1283
+ it('should return session restoration state', () => {
1284
+ const state = authService.getSessionRestorationState();
1285
+
1286
+ expect(state).toHaveProperty('isRestoring');
1287
+ expect(state).toHaveProperty('restorationComplete');
1288
+ expect(state).toHaveProperty('restorationError');
1289
+ });
1290
+
1291
+ it('should return copy of restoration state', () => {
1292
+ const state1 = authService.getSessionRestorationState();
1293
+ const state2 = authService.getSessionRestorationState();
1294
+
1295
+ // Should be different objects (copies)
1296
+ expect(state1).not.toBe(state2);
1297
+ expect(state1).toEqual(state2);
1298
+ });
1299
+ });
827
1300
  });