import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { mockImplementationWhenArgs, spyOnWithOrigin } from '@/testing/spy'; import { vi } from 'vitest'; import { LogLevel } from '../../logging/log-level'; import { LoggerService } from '../../logging/logger.service'; import { mockProvider } from '../../testing/mock'; import type { OpenIdConfiguration } from '../openid-configuration'; import { ConfigValidationService } from './config-validation.service'; import { allMultipleConfigRules } from './rules'; describe('Config Validation Service', () => { let configValidationService: ConfigValidationService; let loggerService: LoggerService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ConfigValidationService, mockProvider(LoggerService)], }); configValidationService = TestBed.inject(ConfigValidationService); loggerService = TestBed.inject(LoggerService); }); const VALID_CONFIG = { authority: 'https://offeringsolutions-sts.azurewebsites.net', redirectUrl: window.location.origin, postLogoutRedirectUri: window.location.origin, clientId: 'angularClient', scope: 'openid profile email', responseType: 'code', silentRenew: true, silentRenewUrl: `${window.location.origin}/silent-renew.html`, renewTimeBeforeTokenExpiresInSeconds: 10, logLevel: LogLevel.Debug, }; it('should create', () => { expect(configValidationService).toBeTruthy(); }); it('should return false for empty config', () => { const config = {}; const result = configValidationService.validateConfig(config); expect(result).toBeFalsy(); }); it('should return true for valid config', () => { const result = configValidationService.validateConfig(VALID_CONFIG); expect(result).toBeTruthy(); }); it('calls `logWarning` if one rule has warning level', () => { const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); const messageTypeSpy = spyOnWithOrigin( configValidationService, 'getAllMessagesOfType' as any ); mockImplementationWhenArgs( messageTypeSpy, (arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2), () => ['A warning message'] ); configValidationService.validateConfig(VALID_CONFIG); expect(loggerWarningSpy).toHaveBeenCalled(); }); describe('ensure-clientId.rule', () => { it('return false when no clientId is set', () => { const config = { ...VALID_CONFIG, clientId: '' } as OpenIdConfiguration; const result = configValidationService.validateConfig(config); expect(result).toBeFalsy(); }); }); describe('ensure-authority-server.rule', () => { it('return false when no security token service is set', () => { const config = { ...VALID_CONFIG, authority: '', } as OpenIdConfiguration; const result = configValidationService.validateConfig(config); expect(result).toBeFalsy(); }); }); describe('ensure-redirect-url.rule', () => { it('return false for no redirect Url', () => { const config = { ...VALID_CONFIG, redirectUrl: '' }; const result = configValidationService.validateConfig(config); expect(result).toBeFalsy(); }); }); describe('ensureSilentRenewUrlWhenNoRefreshTokenUsed', () => { it('return false when silent renew is used with no useRefreshToken and no silentrenewUrl', () => { const config = { ...VALID_CONFIG, silentRenew: true, useRefreshToken: false, silentRenewUrl: '', } as OpenIdConfiguration; const result = configValidationService.validateConfig(config); expect(result).toBeFalsy(); }); }); describe('use-offline-scope-with-silent-renew.rule', () => { it('return true but warning when silent renew is used with useRefreshToken but no offline_access scope is given', () => { const config = { ...VALID_CONFIG, silentRenew: true, useRefreshToken: true, scopes: 'scope1 scope2 but_no_offline_access', }; const loggerSpy = vi.spyOn(loggerService, 'logError'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); const result = configValidationService.validateConfig(config); expect(result).toBeTruthy(); expect(loggerSpy).not.toHaveBeenCalled(); expect(loggerWarningSpy).toHaveBeenCalled(); }); }); describe('ensure-no-duplicated-configs.rule', () => { it('should print out correct error when mutiple configs with same properties are passed', () => { const config1 = { ...VALID_CONFIG, silentRenew: true, useRefreshToken: true, scopes: 'scope1 scope2 but_no_offline_access', }; const config2 = { ...VALID_CONFIG, silentRenew: true, useRefreshToken: true, scopes: 'scope1 scope2 but_no_offline_access', }; const loggerErrorSpy = vi.spyOn(loggerService, 'logError'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); const result = configValidationService.validateConfigs([ config1, config2, ]); expect(result).toBeTruthy(); expect(loggerErrorSpy).not.toHaveBeenCalled(); expect(vi.mocked(loggerWarningSpy).mock.calls[0]).toEqual([ config1, 'You added multiple configs with the same authority, clientId and scope', ]); expect(vi.mocked(loggerWarningSpy).mock.calls[1]).toEqual([ config2, 'You added multiple configs with the same authority, clientId and scope', ]); }); it('should return false and a better error message when config is not passed as object with config property', () => { const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); const result = configValidationService.validateConfigs([]); expect(result).toBeFalsy(); expect(loggerWarningSpy).not.toHaveBeenCalled(); }); }); describe('validateConfigs', () => { it('calls internal method with empty array if something falsy is passed', () => { const spy = vi.spyOn( configValidationService as any, 'validateConfigsInternal' ); const result = configValidationService.validateConfigs([]); expect(result).toBeFalsy(); expect(spy).toHaveBeenCalledExactlyOnceWith([], allMultipleConfigRules); }); }); });