@jmruthers/pace-core 0.5.87 → 0.5.89
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.
- package/dist/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
- package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
- package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
- package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
- package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
- package/dist/auth-DReDSLq9.d.ts +16 -0
- package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
- package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
- package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
- package/dist/{chunk-ZFLOV3OM.js → chunk-7VJDS5QD.js} +401 -16
- package/dist/chunk-7VJDS5QD.js.map +1 -0
- package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
- package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
- package/dist/chunk-BDZUMRBD.js.map +1 -0
- package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
- package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
- package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
- package/dist/chunk-DP5X5ORK.js.map +1 -0
- package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
- package/dist/{chunk-2FQEQUJT.js → chunk-KWICIQVK.js} +4 -4
- package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
- package/dist/chunk-XJ2HZOBU.js.map +1 -0
- package/dist/{chunk-I7O3RSMN.js → chunk-YWAFPVJA.js} +1298 -769
- package/dist/chunk-YWAFPVJA.js.map +1 -0
- package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
- package/dist/components.d.ts +6 -55
- package/dist/components.js +24 -205
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +152 -26
- package/dist/index.js +64 -194
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +5 -3
- package/dist/providers.js +3 -3
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -1
- package/dist/types.js +3 -3
- package/dist/utils.js +2 -2
- package/docs/DOCUMENTATION_AUDIT.md +6 -6
- package/docs/DOCUMENTATION_STANDARD.md +137 -0
- package/docs/README.md +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +83 -40
- package/docs/api/enums/FileCategory.md +56 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +11 -11
- package/docs/api/interfaces/FileDisplayProps.md +10 -10
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +8 -8
- package/docs/api/interfaces/FileUploadProps.md +137 -42
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +83 -50
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
- package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +290 -95
- package/docs/api-reference/components.md +1 -18
- package/docs/api-reference/hooks.md +1 -4
- package/docs/best-practices/testing.md +2 -0
- package/docs/documentation-index.md +1 -1
- package/docs/getting-started/faq.md +1 -1
- package/docs/implementation-guides/file-reference-system.md +592 -58
- package/docs/implementation-guides/file-upload-storage.md +137 -73
- package/docs/implementation-guides/public-pages-advanced.md +10 -0
- package/docs/rbac/super-admin-guide.md +18 -70
- package/docs/testing/README.md +2 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +674 -0
- package/src/__tests__/helpers/test-utils.tsx +3 -2
- package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
- package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
- package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
- package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
- package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
- package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
- package/src/components/FileDisplay/index.tsx +4 -0
- package/src/components/FileUpload/FileUpload.test.tsx +171 -621
- package/src/components/FileUpload/FileUpload.tsx +512 -168
- package/src/components/FileUpload/index.tsx +4 -0
- package/src/components/Progress/Progress.test.tsx +38 -0
- package/src/components/PublicLayout/EventLogo.tsx +6 -4
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/SessionRestorationLoader.tsx +48 -0
- package/src/components/Toast/Toast.tsx +13 -8
- package/src/components/index.ts +16 -16
- package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
- package/src/hooks/public/usePublicEventLogo.ts +16 -20
- package/src/hooks/useEventLogo.ts +316 -0
- package/src/hooks/useEvents.ts +0 -5
- package/src/hooks/useFileReference.test.ts +659 -0
- package/src/hooks/useFileReference.ts +207 -3
- package/src/hooks/useSessionRestoration.ts +64 -0
- package/src/index.ts +17 -5
- package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
- package/src/providers/services/AuthServiceProvider.tsx +27 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
- package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
- package/src/services/AuthService.ts +142 -20
- package/src/services/EventService.ts +0 -4
- package/src/types/auth.ts +15 -0
- package/src/types/file-reference.ts +73 -1
- package/src/types/index.ts +1 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
- package/src/utils/appNameResolver.simple.test.ts +99 -29
- package/src/utils/file-reference.test.ts +535 -0
- package/src/utils/file-reference.ts +200 -30
- package/src/utils/organisationContext.test.ts +5 -19
- package/src/utils/organisationContext.ts +3 -5
- package/src/utils/storage/README.md +269 -262
- package/src/utils/storage/config.ts +9 -0
- package/src/utils/storage/helpers.test.ts +735 -0
- package/src/utils/storage/helpers.ts +189 -16
- package/src/utils/storage/index.ts +3 -0
- package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
- package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
- package/src/validation/__tests__/user.unit.test.ts +1 -1
- package/dist/chunk-5BN3YGNK.js.map +0 -1
- package/dist/chunk-CVMVPYAL.js.map +0 -1
- package/dist/chunk-I7O3RSMN.js.map +0 -1
- package/dist/chunk-WUXCWRL6.js.map +0 -1
- package/dist/chunk-ZFLOV3OM.js.map +0 -1
- package/docs/CONTENT_AUDIT_REPORT.md +0 -253
- package/docs/STYLE_GUIDE.md +0 -37
- package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
- package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
- package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
- package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
- package/src/components/FileUpload/FileUpload.example.tsx +0 -218
- package/src/components/FileUpload/index.ts +0 -6
- package/src/components/FileUpload.tsx +0 -176
- package/src/components/Progress/index.ts +0 -3
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
- package/src/components/SuperAdminGuard.tsx +0 -116
- package/src/components/__tests__/FileDisplay.test.tsx +0 -575
- package/src/components/__tests__/FileUpload.test.tsx +0 -446
- package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
- package/src/components/examples/PermissionExample.tsx +0 -173
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/utils/__tests__/file-reference.test.ts +0 -383
- /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
- /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
- /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
- /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
- /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
- /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
- /package/dist/{chunk-2FQEQUJT.js.map → chunk-KWICIQVK.js.map} +0 -0
- /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
- /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Storage Helpers Tests
|
|
3
|
+
* @description Comprehensive tests for storage utility functions following TEST_STANDARD.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
uploadFile,
|
|
9
|
+
deleteFile,
|
|
10
|
+
downloadFile,
|
|
11
|
+
getPublicUrl,
|
|
12
|
+
getSignedUrl,
|
|
13
|
+
generateFilePath
|
|
14
|
+
} from './helpers';
|
|
15
|
+
import { getBucketName } from './config';
|
|
16
|
+
import { FileCategory } from '../../types/file-reference';
|
|
17
|
+
import { createMockSupabaseClient } from '../../__tests__/helpers/test-utils';
|
|
18
|
+
|
|
19
|
+
// Setup mocks
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Test data
|
|
29
|
+
const mockSupabase = createMockSupabaseClient();
|
|
30
|
+
const createTestFile = (name = 'test.pdf', type = 'application/pdf', size = 1024) => {
|
|
31
|
+
return new File(['test content'], name, { type, size });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('[utility] Storage Helpers', () => {
|
|
35
|
+
describe('getBucketName', () => {
|
|
36
|
+
it('returns public-files bucket for public files', () => {
|
|
37
|
+
expect(getBucketName(true)).toBe('public-files');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns files bucket for private files', () => {
|
|
41
|
+
expect(getBucketName(false)).toBe('files');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('generateFilePath', () => {
|
|
46
|
+
it('generates correct file path with org and customPath', () => {
|
|
47
|
+
const result = generateFilePath(
|
|
48
|
+
{ orgId: 'test-org-123', isPublic: false, customPath: 'general_documents' },
|
|
49
|
+
'test-document.pdf'
|
|
50
|
+
);
|
|
51
|
+
expect(result).toBe('test-org-123/general_documents/test-document.pdf');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('generates unique paths for same file name', () => {
|
|
55
|
+
const path1 = generateFilePath({ orgId: 'org-123', customPath: 'images' }, 'test.jpg');
|
|
56
|
+
const path2 = generateFilePath({ orgId: 'org-123', customPath: 'images' }, 'test.jpg');
|
|
57
|
+
|
|
58
|
+
expect(path1).toBe(path2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('validates required orgId', () => {
|
|
62
|
+
expect(() => generateFilePath({ orgId: '' } as any, 'test.jpg'))
|
|
63
|
+
.toThrow('orgId is required for file path generation');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles special characters in file names', () => {
|
|
67
|
+
const result = generateFilePath(
|
|
68
|
+
{ orgId: 'org-123', customPath: 'images' },
|
|
69
|
+
'test file with spaces & symbols.jpg'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result).toBe('org-123/images/test file with spaces & symbols.jpg');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('preserves file extension', () => {
|
|
76
|
+
const testCases = [
|
|
77
|
+
{ fileName: 'test.pdf', expectedExt: '.pdf' },
|
|
78
|
+
{ fileName: 'image.jpeg', expectedExt: '.jpeg' },
|
|
79
|
+
{ fileName: 'document.docx', expectedExt: '.docx' },
|
|
80
|
+
{ fileName: 'noextension', expectedExt: '' }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
testCases.forEach(({ fileName, expectedExt }) => {
|
|
84
|
+
const result = generateFilePath({ orgId: 'org-123', customPath: 'general_documents' }, fileName);
|
|
85
|
+
expect(result).toMatch(new RegExp(`${expectedExt.replace('.', '\\.')}$`));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('uploadFile', () => {
|
|
91
|
+
it('uploads file to correct bucket successfully', async () => {
|
|
92
|
+
const testFile = createTestFile();
|
|
93
|
+
const options = {
|
|
94
|
+
orgId: 'test-org-123',
|
|
95
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
96
|
+
isPublic: false
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
mockSupabase.storage = {
|
|
100
|
+
from: vi.fn(() => ({
|
|
101
|
+
upload: vi.fn().mockResolvedValue({
|
|
102
|
+
data: { path: 'test-path' },
|
|
103
|
+
error: null
|
|
104
|
+
})
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = await uploadFile(mockSupabase, testFile, options);
|
|
109
|
+
|
|
110
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
expect(result.path).toMatch(/^test-org-123\//);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('uploads to public bucket when isPublic is true', async () => {
|
|
116
|
+
const testFile = createTestFile();
|
|
117
|
+
const options = {
|
|
118
|
+
orgId: 'test-org-123',
|
|
119
|
+
category: FileCategory.EVENT_LOGOS,
|
|
120
|
+
isPublic: true
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
mockSupabase.storage = {
|
|
124
|
+
from: vi.fn(() => ({
|
|
125
|
+
upload: vi.fn().mockResolvedValue({
|
|
126
|
+
data: { path: 'test-path' },
|
|
127
|
+
error: null
|
|
128
|
+
})
|
|
129
|
+
}))
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await uploadFile(mockSupabase, testFile, options);
|
|
133
|
+
|
|
134
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('handles upload errors gracefully', async () => {
|
|
138
|
+
const testFile = createTestFile();
|
|
139
|
+
const options = {
|
|
140
|
+
orgId: 'test-org-123',
|
|
141
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
142
|
+
isPublic: false
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
mockSupabase.storage = {
|
|
146
|
+
from: vi.fn(() => ({
|
|
147
|
+
upload: vi.fn().mockResolvedValue({
|
|
148
|
+
data: null,
|
|
149
|
+
error: { message: 'Upload failed' }
|
|
150
|
+
})
|
|
151
|
+
}))
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await uploadFile(mockSupabase, testFile, options);
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
expect(result.error).toBe('Upload failed: Upload failed');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('validates required parameters', async () => {
|
|
161
|
+
const testFile = createTestFile();
|
|
162
|
+
|
|
163
|
+
const noClient = await uploadFile(null as any, testFile, {} as any);
|
|
164
|
+
expect(noClient.success).toBe(false);
|
|
165
|
+
|
|
166
|
+
const noFile = await uploadFile(mockSupabase, null as any, {} as any);
|
|
167
|
+
expect(noFile.success).toBe(false);
|
|
168
|
+
|
|
169
|
+
const noOrg = await uploadFile(mockSupabase, testFile, { orgId: '' } as any);
|
|
170
|
+
expect(noOrg.success).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('includes file metadata in result', async () => {
|
|
174
|
+
const testFile = createTestFile('test.jpg', 'image/jpeg', 2048);
|
|
175
|
+
const options = {
|
|
176
|
+
orgId: 'test-org-123',
|
|
177
|
+
category: FileCategory.IMAGES,
|
|
178
|
+
isPublic: false
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
mockSupabase.storage = {
|
|
182
|
+
from: vi.fn(() => ({
|
|
183
|
+
upload: vi.fn().mockResolvedValue({
|
|
184
|
+
data: { path: 'test-path' },
|
|
185
|
+
error: null
|
|
186
|
+
})
|
|
187
|
+
}))
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = await uploadFile(mockSupabase, testFile, options);
|
|
191
|
+
|
|
192
|
+
expect(result.metadata.mimeType).toBe('image/jpeg');
|
|
193
|
+
expect(result.metadata.size).toBe(testFile.size);
|
|
194
|
+
expect(result.metadata.orgId).toBe('test-org-123');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('uses custom path when provided', async () => {
|
|
198
|
+
const testFile = createTestFile();
|
|
199
|
+
const options = {
|
|
200
|
+
orgId: 'test-org-123',
|
|
201
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
202
|
+
isPublic: false,
|
|
203
|
+
customPath: 'custom/path/file.pdf'
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const mockUpload = vi.fn().mockResolvedValue({
|
|
207
|
+
data: { path: 'custom/path/file.pdf' },
|
|
208
|
+
error: null
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
mockSupabase.storage = {
|
|
212
|
+
from: vi.fn(() => ({ upload: mockUpload }))
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await uploadFile(mockSupabase, testFile, options);
|
|
216
|
+
|
|
217
|
+
expect(mockUpload).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('deleteFile', () => {
|
|
222
|
+
it('deletes file from correct bucket successfully', async () => {
|
|
223
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
224
|
+
|
|
225
|
+
const mockRemove = vi.fn().mockResolvedValue({
|
|
226
|
+
data: null,
|
|
227
|
+
error: null
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
mockSupabase.storage = {
|
|
231
|
+
from: vi.fn(() => ({ remove: mockRemove }))
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = await deleteFile(mockSupabase, filePath, false);
|
|
235
|
+
|
|
236
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
|
|
237
|
+
expect(mockRemove).toHaveBeenCalledWith([filePath]);
|
|
238
|
+
expect(result.success).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('deletes from public bucket when isPublic is true', async () => {
|
|
242
|
+
const filePath = 'org-123/logos/logo.png';
|
|
243
|
+
|
|
244
|
+
const mockRemove = vi.fn().mockResolvedValue({
|
|
245
|
+
data: null,
|
|
246
|
+
error: null
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
mockSupabase.storage = {
|
|
250
|
+
from: vi.fn(() => ({ remove: mockRemove }))
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await deleteFile(mockSupabase, filePath, true);
|
|
254
|
+
|
|
255
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('handles delete errors gracefully', async () => {
|
|
259
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
260
|
+
|
|
261
|
+
const mockRemove = vi.fn().mockResolvedValue({
|
|
262
|
+
data: null,
|
|
263
|
+
error: { message: 'File not found' }
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
mockSupabase.storage = {
|
|
267
|
+
from: vi.fn(() => ({ remove: mockRemove }))
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const result = await deleteFile(mockSupabase, filePath, false);
|
|
271
|
+
|
|
272
|
+
expect(result.success).toBe(false);
|
|
273
|
+
expect(result.error).toBe('Delete failed: File not found');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('validates required parameters', async () => {
|
|
277
|
+
const delNoClient = await deleteFile(null as any, 'path', false);
|
|
278
|
+
expect(delNoClient.success).toBe(false);
|
|
279
|
+
|
|
280
|
+
const delNoPath = await deleteFile(mockSupabase, '', false);
|
|
281
|
+
expect(delNoPath.success).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('downloadFile', () => {
|
|
286
|
+
it('downloads file from correct bucket successfully', async () => {
|
|
287
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
288
|
+
const mockBlob = new Blob(['file content'], { type: 'application/pdf' });
|
|
289
|
+
|
|
290
|
+
const mockDownload = vi.fn().mockResolvedValue({
|
|
291
|
+
data: mockBlob,
|
|
292
|
+
error: null
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const mockList = vi.fn().mockResolvedValue({
|
|
296
|
+
data: [{ metadata: { size: mockBlob.size, mimetype: mockBlob.type } }],
|
|
297
|
+
error: null
|
|
298
|
+
});
|
|
299
|
+
mockSupabase.storage = {
|
|
300
|
+
from: vi.fn(() => ({ download: mockDownload, list: mockList }))
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = await downloadFile(mockSupabase, filePath, false);
|
|
304
|
+
|
|
305
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
|
|
306
|
+
expect(mockDownload).toHaveBeenCalledWith(filePath);
|
|
307
|
+
expect(result?.blob).toBe(mockBlob);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('downloads from public bucket when isPublic is true', async () => {
|
|
311
|
+
const filePath = 'org-123/logos/logo.png';
|
|
312
|
+
const mockBlob = new Blob(['image content'], { type: 'image/png' });
|
|
313
|
+
|
|
314
|
+
const mockDownload = vi.fn().mockResolvedValue({
|
|
315
|
+
data: mockBlob,
|
|
316
|
+
error: null
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
mockSupabase.storage = {
|
|
320
|
+
from: vi.fn(() => ({ download: mockDownload }))
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
await downloadFile(mockSupabase, filePath, true);
|
|
324
|
+
|
|
325
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns null when download fails', async () => {
|
|
329
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
330
|
+
|
|
331
|
+
const mockDownload = vi.fn().mockResolvedValue({
|
|
332
|
+
data: null,
|
|
333
|
+
error: { message: 'File not found' }
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
mockSupabase.storage = {
|
|
337
|
+
from: vi.fn(() => ({ download: mockDownload }))
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const result = await downloadFile(mockSupabase, filePath, false);
|
|
341
|
+
|
|
342
|
+
expect(result).toBe(null);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('validates required parameters', async () => {
|
|
346
|
+
const dlNoClient = await downloadFile(null as any, 'path', false);
|
|
347
|
+
expect(dlNoClient).toBe(null);
|
|
348
|
+
|
|
349
|
+
const dlNoPath = await downloadFile(mockSupabase, '', false);
|
|
350
|
+
expect(dlNoPath).toBe(null);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('getPublicUrl', () => {
|
|
355
|
+
it('generates public URL for public bucket', () => {
|
|
356
|
+
const filePath = 'org-123/logos/logo.png';
|
|
357
|
+
|
|
358
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
359
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
mockSupabase.storage = {
|
|
363
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
367
|
+
|
|
368
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
369
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
|
|
370
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns direct URLs without calling storage', () => {
|
|
374
|
+
const directUrl = 'https://cdn.example.com/logo.png';
|
|
375
|
+
|
|
376
|
+
const localSupabase = createMockSupabaseClient();
|
|
377
|
+
localSupabase.storage = {
|
|
378
|
+
from: vi.fn()
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const result = getPublicUrl(localSupabase as any, directUrl, true);
|
|
382
|
+
|
|
383
|
+
expect(localSupabase.storage.from).not.toHaveBeenCalled();
|
|
384
|
+
expect(result).toBe(directUrl);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('generates public URL for private bucket', () => {
|
|
388
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
389
|
+
|
|
390
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
391
|
+
data: { publicUrl: 'https://example.com/files/test.pdf' }
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
mockSupabase.storage = {
|
|
395
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const result = getPublicUrl(mockSupabase, filePath, false);
|
|
399
|
+
|
|
400
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
|
|
401
|
+
expect(result).toBe('https://example.com/files/test.pdf');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('uses explicit bucket prefix when provided', () => {
|
|
405
|
+
const filePath = 'public-files/org-123/logo.png';
|
|
406
|
+
|
|
407
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
408
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
mockSupabase.storage = {
|
|
412
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
416
|
+
|
|
417
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
418
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
419
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('supports custom bucket hints without hyphen characters', () => {
|
|
423
|
+
const filePath = 'branding/org-123/logo.png';
|
|
424
|
+
|
|
425
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
426
|
+
data: { publicUrl: 'https://example.com/branding/logo.png' }
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
mockSupabase.storage = {
|
|
430
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
434
|
+
|
|
435
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('branding');
|
|
436
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
437
|
+
expect(result).toBe('https://example.com/branding/logo.png');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('supports double colon bucket hints to avoid path ambiguity', () => {
|
|
441
|
+
const filePath = 'brand-assets::org-123/logo.png';
|
|
442
|
+
|
|
443
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
444
|
+
data: { publicUrl: 'https://example.com/brand/logo.png' }
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
mockSupabase.storage = {
|
|
448
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
452
|
+
|
|
453
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('brand-assets');
|
|
454
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
455
|
+
expect(result).toBe('https://example.com/brand/logo.png');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('treats numeric leading path segments as directories rather than buckets', () => {
|
|
459
|
+
const filePath = '2024/05/logo.png';
|
|
460
|
+
|
|
461
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
462
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
mockSupabase.storage = {
|
|
466
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
470
|
+
|
|
471
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
472
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
|
|
473
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('ignores bucket prefix when path starts with UUID', () => {
|
|
477
|
+
const filePath = '123e4567-e89b-12d3-a456-426614174000/event_logos/logo.png';
|
|
478
|
+
|
|
479
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
480
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
mockSupabase.storage = {
|
|
484
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
488
|
+
|
|
489
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
490
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
|
|
491
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('validates required parameters', () => {
|
|
495
|
+
expect(() => getPublicUrl(null as any, 'path', false))
|
|
496
|
+
.toThrow();
|
|
497
|
+
|
|
498
|
+
expect(() => getPublicUrl(mockSupabase, '', false))
|
|
499
|
+
.toThrow();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('getSignedUrl', () => {
|
|
504
|
+
it('generates signed URL with default expiration', async () => {
|
|
505
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
506
|
+
const options = {
|
|
507
|
+
appName: 'test-app',
|
|
508
|
+
orgId: 'org-123'
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const mockCreateSignedUrl = vi.fn().mockResolvedValue({
|
|
512
|
+
data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
|
|
513
|
+
error: null
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
mockSupabase.storage = {
|
|
517
|
+
from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const result = await getSignedUrl(mockSupabase, filePath, options);
|
|
521
|
+
|
|
522
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('files');
|
|
523
|
+
expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 3600);
|
|
524
|
+
expect(result?.url).toBe('https://example.com/signed/test.pdf?token=abc123');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('generates signed URL with custom expiration', async () => {
|
|
528
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
529
|
+
const options = {
|
|
530
|
+
appName: 'test-app',
|
|
531
|
+
orgId: 'org-123',
|
|
532
|
+
expiresIn: 7200
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const mockCreateSignedUrl = vi.fn().mockResolvedValue({
|
|
536
|
+
data: { signedUrl: 'https://example.com/signed/test.pdf?token=def456' },
|
|
537
|
+
error: null
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
mockSupabase.storage = {
|
|
541
|
+
from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
await getSignedUrl(mockSupabase, filePath, options);
|
|
545
|
+
|
|
546
|
+
expect(mockCreateSignedUrl).toHaveBeenCalledWith(filePath, 7200);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('returns null when signed URL generation fails', async () => {
|
|
550
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
551
|
+
const options = {
|
|
552
|
+
appName: 'test-app',
|
|
553
|
+
orgId: 'org-123'
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const mockCreateSignedUrl = vi.fn().mockResolvedValue({
|
|
557
|
+
data: null,
|
|
558
|
+
error: { message: 'Permission denied' }
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
mockSupabase.storage = {
|
|
562
|
+
from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const result = await getSignedUrl(mockSupabase, filePath, options);
|
|
566
|
+
|
|
567
|
+
expect(result).toBe(null);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('validates required parameters', async () => {
|
|
571
|
+
const options = { appName: 'test-app', orgId: 'org-123' };
|
|
572
|
+
|
|
573
|
+
const gsNoClient = await getSignedUrl(null as any, 'path', options);
|
|
574
|
+
expect(gsNoClient).toBe(null);
|
|
575
|
+
|
|
576
|
+
const gsNoPath = await getSignedUrl(mockSupabase, '', options);
|
|
577
|
+
expect(gsNoPath).toBe(null);
|
|
578
|
+
|
|
579
|
+
const gsNoOpts = await getSignedUrl(mockSupabase, 'path', {} as any);
|
|
580
|
+
expect(gsNoOpts).toBe(null);
|
|
581
|
+
|
|
582
|
+
const gsNoOrg = await getSignedUrl(mockSupabase, 'path', { appName: 'test' } as any);
|
|
583
|
+
expect(gsNoOrg).toBe(null);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('includes expiration metadata in result', async () => {
|
|
587
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
588
|
+
const options = {
|
|
589
|
+
appName: 'test-app',
|
|
590
|
+
orgId: 'org-123',
|
|
591
|
+
expiresIn: 3600
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const mockCreateSignedUrl = vi.fn().mockResolvedValue({
|
|
595
|
+
data: { signedUrl: 'https://example.com/signed/test.pdf?token=abc123' },
|
|
596
|
+
error: null
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
mockSupabase.storage = {
|
|
600
|
+
from: vi.fn(() => ({ createSignedUrl: mockCreateSignedUrl }))
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const result = await getSignedUrl(mockSupabase, filePath, options);
|
|
604
|
+
|
|
605
|
+
expect(result?.url).toContain('signed');
|
|
606
|
+
expect(typeof result?.expiresAt).toBe('string');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe('Error Handling', () => {
|
|
611
|
+
it('handles storage client errors gracefully', async () => {
|
|
612
|
+
const testFile = createTestFile();
|
|
613
|
+
const options = {
|
|
614
|
+
orgId: 'test-org-123',
|
|
615
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
616
|
+
isPublic: false
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
mockSupabase.storage = {
|
|
620
|
+
from: vi.fn(() => {
|
|
621
|
+
throw new Error('Storage client error');
|
|
622
|
+
})
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const res = await uploadFile(mockSupabase, testFile, options);
|
|
626
|
+
expect(res.success).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('handles network timeouts gracefully', async () => {
|
|
630
|
+
const filePath = 'org-123/documents/test.pdf';
|
|
631
|
+
|
|
632
|
+
const mockDownload = vi.fn().mockRejectedValue(new Error('Network timeout'));
|
|
633
|
+
|
|
634
|
+
mockSupabase.storage = {
|
|
635
|
+
from: vi.fn(() => ({ download: mockDownload }))
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const dl = await downloadFile(mockSupabase, filePath, false);
|
|
639
|
+
expect(dl).toBe(null);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
describe('Integration Scenarios', () => {
|
|
644
|
+
it('handles complete upload-download cycle', async () => {
|
|
645
|
+
const testFile = createTestFile('integration-test.pdf', 'application/pdf', 2048);
|
|
646
|
+
const uploadOptions = {
|
|
647
|
+
orgId: 'test-org-123',
|
|
648
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
649
|
+
isPublic: false
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Mock upload
|
|
653
|
+
const mockUpload = vi.fn().mockResolvedValue({
|
|
654
|
+
data: { path: 'org-123/documents/integration-test.pdf' },
|
|
655
|
+
error: null
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Mock download
|
|
659
|
+
const mockDownload = vi.fn().mockResolvedValue({
|
|
660
|
+
data: new Blob(['file content'], { type: 'application/pdf' }),
|
|
661
|
+
error: null
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const mockList = vi.fn().mockResolvedValue({ data: [{ metadata: { size: 2048, mimetype: 'application/pdf' } }], error: null });
|
|
665
|
+
mockSupabase.storage = {
|
|
666
|
+
from: vi.fn(() => ({
|
|
667
|
+
upload: mockUpload,
|
|
668
|
+
download: mockDownload,
|
|
669
|
+
list: mockList
|
|
670
|
+
}))
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Upload file
|
|
674
|
+
const uploadResult = await uploadFile(mockSupabase, testFile, uploadOptions);
|
|
675
|
+
expect(uploadResult.success).toBe(true);
|
|
676
|
+
|
|
677
|
+
// Download file
|
|
678
|
+
const downloadResult = await downloadFile(mockSupabase, uploadResult.path!, false);
|
|
679
|
+
expect(downloadResult?.blob).toBeInstanceOf(Blob);
|
|
680
|
+
expect(downloadResult?.metadata.type).toBe('application/pdf');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('handles public vs private file workflows', async () => {
|
|
684
|
+
const publicFile = createTestFile('public.jpg', 'image/jpeg');
|
|
685
|
+
const privateFile = createTestFile('private.pdf', 'application/pdf');
|
|
686
|
+
|
|
687
|
+
const mockUpload = vi.fn().mockResolvedValue({
|
|
688
|
+
data: { path: 'test-path' },
|
|
689
|
+
error: null
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
693
|
+
data: { publicUrl: 'https://example.com/public.jpg' }
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const mockCreateSignedUrl = vi.fn().mockResolvedValue({
|
|
697
|
+
data: { signedUrl: 'https://example.com/signed/private.pdf?token=abc' },
|
|
698
|
+
error: null
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
mockSupabase.storage = {
|
|
702
|
+
from: vi.fn(() => ({
|
|
703
|
+
upload: mockUpload,
|
|
704
|
+
getPublicUrl: mockGetPublicUrl,
|
|
705
|
+
createSignedUrl: mockCreateSignedUrl
|
|
706
|
+
}))
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Upload public file
|
|
710
|
+
await uploadFile(mockSupabase, publicFile, {
|
|
711
|
+
orgId: 'org-123',
|
|
712
|
+
category: FileCategory.IMAGES,
|
|
713
|
+
isPublic: true
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Upload private file
|
|
717
|
+
await uploadFile(mockSupabase, privateFile, {
|
|
718
|
+
orgId: 'org-123',
|
|
719
|
+
category: FileCategory.GENERAL_DOCUMENTS,
|
|
720
|
+
isPublic: false
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Get public URL
|
|
724
|
+
const publicUrl = getPublicUrl(mockSupabase, 'org-123/images/public.jpg', true);
|
|
725
|
+
expect(publicUrl).toBe('https://example.com/public.jpg');
|
|
726
|
+
|
|
727
|
+
// Get signed URL
|
|
728
|
+
const signedResult = await getSignedUrl(mockSupabase, 'org-123/documents/private.pdf', {
|
|
729
|
+
appName: 'test-app',
|
|
730
|
+
orgId: 'org-123'
|
|
731
|
+
});
|
|
732
|
+
expect(signedResult?.url).toBe('https://example.com/signed/private.pdf?token=abc');
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
});
|