Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 54 additions & 25 deletions src/app/core/guards/is-file-provider.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,78 @@
import { ParamMap, UrlSegment } from '@angular/router';
import { MockProvider } from 'ng-mocks';

import { Mock } from 'vitest';

import { TestBed } from '@angular/core/testing';
import { Route, UrlSegment } from '@angular/router';

import { FileProviderRegistryService } from '@core/services/file-provider-registry.service';
import { FileProvider } from '@osf/features/files/constants';

import { isFileProvider } from './is-file-provider.guard';

describe('isFileProvider', () => {
const createMockParamMap = (): ParamMap => ({
get: () => null,
getAll: () => [],
has: () => false,
keys: [],
});
let registry: { isValidProvider: Mock };

const createMockSegment = (path: string): UrlSegment => ({
path,
parameters: {},
parameterMap: createMockParamMap(),
});
const FOREIGN_PROVIDER = 's3compat';
const route: Route = {};

const createSegments = (...paths: string[]): UrlSegment[] => paths.map((path) => new UrlSegment(path, {}));

const createMockSegments = (path: string) => [createMockSegment(path)];
const runGuard = (segments: UrlSegment[]) => TestBed.runInInjectionContext(() => isFileProvider(route, segments));

it('should return true when id matches a FileProvider value', () => {
beforeEach(() => {
const validProviders: string[] = [...Object.values(FileProvider), FOREIGN_PROVIDER];

registry = {
isValidProvider: vi.fn((providerName: string) => validProviders.includes(providerName.toLowerCase())),
};

TestBed.configureTestingModule({
providers: [MockProvider(FileProviderRegistryService, registry)],
});
});

it('should return true when id matches a built-in FileProvider value', () => {
Object.values(FileProvider).forEach((provider) => {
const result = isFileProvider({} as any, createMockSegments(provider));
expect(result).toBe(true);
expect(runGuard(createSegments(provider))).toBe(true);
expect(registry.isValidProvider).toHaveBeenCalledWith(provider);
});
});

it('should return false when id does not match any FileProvider value', () => {
const result = isFileProvider({} as any, createMockSegments('invalid-provider'));
expect(result).toBe(false);
it('should return true when id matches an external provider registered in gravyvalet', () => {
expect(runGuard(createSegments(FOREIGN_PROVIDER))).toBe(true);
expect(registry.isValidProvider).toHaveBeenCalledWith(FOREIGN_PROVIDER);
});

it('should return false when id does not match any registered provider', () => {
expect(runGuard(createSegments('invalid-provider'))).toBe(false);
expect(registry.isValidProvider).toHaveBeenCalledWith('invalid-provider');
});

it('should return false when the registry has no providers', () => {
registry.isValidProvider.mockReturnValue(false);

expect(runGuard(createSegments(FileProvider.OsfStorage))).toBe(false);
});

it('should only check the first segment', () => {
expect(runGuard(createSegments(FileProvider.GoogleDrive, 'subfolder', 'file.txt'))).toBe(true);
expect(registry.isValidProvider).toHaveBeenCalledTimes(1);
expect(registry.isValidProvider).toHaveBeenCalledWith(FileProvider.GoogleDrive);
});

it('should return false when segments array is empty', () => {
const result = isFileProvider({} as any, []);
expect(result).toBe(false);
expect(runGuard([])).toBe(false);
expect(registry.isValidProvider).not.toHaveBeenCalled();
});

it('should return false when first segment has no path', () => {
const result = isFileProvider({} as any, [createMockSegment('')]);
expect(result).toBe(false);
expect(runGuard(createSegments(''))).toBe(false);
expect(registry.isValidProvider).not.toHaveBeenCalled();
});

it('should return false when first segment is undefined', () => {
const result = isFileProvider({} as any, [undefined as any]);
expect(result).toBe(false);
expect(runGuard([undefined as unknown as UrlSegment])).toBe(false);
expect(registry.isValidProvider).not.toHaveBeenCalled();
});
});
15 changes: 13 additions & 2 deletions src/app/core/guards/is-file-provider.guard.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { inject } from '@angular/core';
import { CanMatchFn, Route, UrlSegment } from '@angular/router';

import { FileProvider } from '@osf/features/files/constants';
import { FileProviderRegistryService } from '@core/services/file-provider-registry.service';

/**
* Route guard that checks if a file provider is valid.
* Supports both built-in providers (osfstorage, googledrive, etc.) and
* dynamically discovered external storage services (foreign addons like s3compat).
*/
export const isFileProvider: CanMatchFn = (route: Route, segments: UrlSegment[]) => {
const id = segments[0]?.path;
if (!id) {
return false;
}

return !!(id && Object.values(FileProvider).some((provider) => provider === id));
const registry = inject(FileProviderRegistryService);

return registry.isValidProvider(id);
};
50 changes: 50 additions & 0 deletions src/app/core/provider/application.initialization.provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MockProvider } from 'ng-mocks';
import { PLATFORM_ID } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { FileProviderRegistryService } from '@core/services/file-provider-registry.service';
import { OSFConfigService } from '@core/services/osf-config.service';
import { EnvironmentModel } from '@osf/shared/models/environment.model';

Expand All @@ -26,13 +27,15 @@ vi.mock('@sentry/angular', () => {

describe('initializeApplication', () => {
let configServiceMock: { load: ReturnType<typeof vi.fn> };
let fileProviderRegistryMock: { initialize: ReturnType<typeof vi.fn> };
let googleTagManagerConfigurationMock: { set: ReturnType<typeof vi.fn> };
let environment: EnvironmentModel;
let sentryInitMock: ReturnType<typeof vi.fn>;
let sentryMock: SentryMockType;

function setup(platformId: 'browser' | 'server', environmentOverrides: Partial<EnvironmentModel> = {}) {
configServiceMock = { load: vi.fn().mockResolvedValue(undefined) };
fileProviderRegistryMock = { initialize: vi.fn().mockResolvedValue(undefined) };
googleTagManagerConfigurationMock = { set: vi.fn() };
sentryMock = SentryMock.simple();

Expand All @@ -41,6 +44,7 @@ describe('initializeApplication', () => {
provideOSFCore(),
MockProvider(PLATFORM_ID, platformId),
{ provide: OSFConfigService, useValue: configServiceMock },
{ provide: FileProviderRegistryService, useValue: fileProviderRegistryMock },
{ provide: GoogleTagManagerConfiguration, useValue: googleTagManagerConfigurationMock },
{ provide: SENTRY_TOKEN, useValue: sentryMock },
],
Expand All @@ -61,6 +65,7 @@ describe('initializeApplication', () => {
await TestBed.runInInjectionContext(async () => initializeApplication()());

expect(configServiceMock.load).toHaveBeenCalled();
expect(fileProviderRegistryMock.initialize).toHaveBeenCalled();
expect(googleTagManagerConfigurationMock.set).toHaveBeenCalledWith({ id: 'GTM-TEST' });
expect(sentryInitMock).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -89,7 +94,52 @@ describe('initializeApplication', () => {
await TestBed.runInInjectionContext(async () => initializeApplication()());

expect(configServiceMock.load).toHaveBeenCalled();
expect(fileProviderRegistryMock.initialize).toHaveBeenCalled();
expect(googleTagManagerConfigurationMock.set).not.toHaveBeenCalled();
expect(sentryInitMock).not.toHaveBeenCalled();
});

it('should initialize the file provider registry only after the config is loaded', async () => {
setup('browser');
let resolveConfig: () => void = () => undefined;
configServiceMock.load.mockReturnValue(
new Promise<void>((resolve) => {
resolveConfig = resolve;
})
);

const initialization = TestBed.runInInjectionContext(async () => initializeApplication()());
await new Promise((resolve) => setTimeout(resolve));

expect(configServiceMock.load).toHaveBeenCalled();
expect(fileProviderRegistryMock.initialize).not.toHaveBeenCalled();

resolveConfig();
await initialization;

expect(fileProviderRegistryMock.initialize).toHaveBeenCalledTimes(1);
});

it('should not complete before the file provider registry is initialized', async () => {
setup('browser');
let resolveRegistry: () => void = () => undefined;
fileProviderRegistryMock.initialize.mockReturnValue(
new Promise<void>((resolve) => {
resolveRegistry = resolve;
})
);
let completed = false;

const initialization = TestBed.runInInjectionContext(async () => initializeApplication()()).then(() => {
completed = true;
});
await new Promise((resolve) => setTimeout(resolve));

expect(completed).toBe(false);

resolveRegistry();
await initialization;

expect(completed).toBe(true);
});
});
8 changes: 8 additions & 0 deletions src/app/core/provider/application.initialization.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID, provideAppInitializer } from '@angular/core';

import { SENTRY_DENY_URLS, SENTRY_IGNORE_ERRORS, sentryBeforeSend } from '@core/helpers/sentry-filter.helper';
import { FileProviderRegistryService } from '@core/services/file-provider-registry.service';
import { OSFConfigService } from '@core/services/osf-config.service';

import { ENVIRONMENT } from './environment.provider';
Expand All @@ -24,9 +25,14 @@ export function initializeApplication() {
const configService = inject(OSFConfigService);
const googleTagManagerConfiguration = inject(GoogleTagManagerConfiguration);
const environment = inject(ENVIRONMENT);
const fileProviderRegistry = inject(FileProviderRegistryService);

await configService.load();

// Initialize the file provider registry to fetch external storage services
// This enables foreign addon support
const registryPromise = fileProviderRegistry.initialize();

if (isPlatformBrowser(platformId)) {
const googleTagManagerId = environment.googleTagManagerId;

Expand Down Expand Up @@ -79,6 +85,8 @@ export function initializeApplication() {
};
new BrowserAgent(newRelicConfig);
}

await registryPromise;
};
}

Expand Down
Loading