feat: add more auth features and remove auth module
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled

This commit is contained in:
master 2025-02-08 01:02:00 +08:00
parent c8c4fc847d
commit fe10ed2850
15 changed files with 263 additions and 155 deletions

View File

@ -26,7 +26,7 @@ pnpm add oidc-client-rx @outposts/injection-js @abraham/reflection
```typescript ```typescript
import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect' import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect'
import { type Injector, ReflectiveInjector } from '@outposts/injection-js'; import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
import { LogLevel, OidcSecurityService, provideAuth } from 'oidc-client-rx'; import { LogLevel, OidcSecurityService, provideAuth, withDefaultFeatures } from 'oidc-client-rx';
const injector = ReflectiveInjector.resolveAndCreate( const injector = ReflectiveInjector.resolveAndCreate(
provideAuth( provideAuth(
@ -43,7 +43,8 @@ const injector = ReflectiveInjector.resolveAndCreate(
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
... ...
}, },
} },
withDefaultFeatures()
) )
) as Injector; ) as Injector;

View File

@ -1,7 +1,12 @@
import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect' import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect'
import { type Injector, ReflectiveInjector } from '@outposts/injection-js'; import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
import { RouterProvider, createRouter } from '@tanstack/react-router'; import { RouterProvider, createRouter } from '@tanstack/react-router';
import { LogLevel, OidcSecurityService, provideAuth } from 'oidc-client-rx'; import {
LogLevel,
OidcSecurityService,
provideAuth,
withDefaultFeatures,
} from 'oidc-client-rx';
import { import {
InjectorContextVoidInjector, InjectorContextVoidInjector,
InjectorProvider, InjectorProvider,
@ -51,6 +56,11 @@ const injector = ReflectiveInjector.resolveAndCreate(
}, },
}, },
}, },
withDefaultFeatures(
// the after feature will replace the before same type feature
// so the following line can be ignored
{ router: { enabled: false } }
),
withTanstackRouter(router) withTanstackRouter(router)
) )
) as Injector; ) as Injector;

View File

@ -1,7 +1,7 @@
import { InjectionToken, inject } from '@outposts/injection-js'; import { InjectionToken, inject } from '@outposts/injection-js';
import type { Router } from '@tanstack/react-router'; import type { Router } from '@tanstack/react-router';
import { AbstractRouter } from 'src/router'; import type { AuthFeature } from '../../features';
import type { AuthFeature } from '../../provide-auth'; import { AbstractRouter } from '../../router';
export type TanStackRouter = Router<any, any, any, any, any, any>; export type TanStackRouter = Router<any, any, any, any, any, any>;

View File

@ -1,17 +1,17 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { import {
type DefaultHttpTestingController,
HTTP_CLIENT_TEST_CONTROLLER, HTTP_CLIENT_TEST_CONTROLLER,
provideHttpClientTesting, provideHttpClientTesting,
} from '@/testing/http'; } from '@/testing/http';
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import type { HttpTestingController } from '@ngify/http/testing';
import { ReplaySubject, firstValueFrom, share } from 'rxjs'; import { ReplaySubject, firstValueFrom, share } from 'rxjs';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { HttpBaseService } from './http-base.service'; import { HttpBaseService } from './http-base.service';
describe('Data Service', () => { describe('Data Service', () => {
let dataService: DataService; let dataService: DataService;
let httpMock: HttpTestingController; let httpMock: DefaultHttpTestingController;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@ -1,11 +1,10 @@
import { HttpClient, type HttpHeaders } from '@ngify/http';
import { Injectable, inject } from '@outposts/injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import type { HttpParams } from '../http'; import { HTTP_CLIENT, type HttpHeaders, type HttpParams } from '../http';
@Injectable() @Injectable()
export class HttpBaseService { export class HttpBaseService {
private readonly http = inject(HttpClient); private readonly http = inject(HTTP_CLIENT);
get<T>( get<T>(
url: string, url: string,
@ -22,7 +21,7 @@ export class HttpBaseService {
body: unknown, body: unknown,
options: { headers?: HttpHeaders; params?: HttpParams } = {} options: { headers?: HttpHeaders; params?: HttpParams } = {}
): Observable<T> { ): Observable<T> {
return this.http.post<T>(url, body, { return this.http.post<T>(url, body as any, {
...options, ...options,
params: options.params?.toNgify(), params: options.params?.toNgify(),
}); });

View File

@ -1,66 +0,0 @@
import { TestBed } from '@/testing';
import { of } from 'rxjs';
import { PASSED_CONFIG } from './auth-config';
import { AuthModule } from './auth.module';
import { ConfigurationService } from './config/config.service';
import {
StsConfigHttpLoader,
StsConfigLoader,
StsConfigStaticLoader,
} from './config/loader/config-loader';
import { mockProvider } from './testing/mock';
describe('AuthModule', () => {
describe('APP_CONFIG', () => {
let authModule: AuthModule;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AuthModule.forRoot({ config: { authority: 'something' } })],
providers: [mockProvider(ConfigurationService)],
}).compileComponents();
authModule = TestBed.getImportByType(AuthModule);
});
it('should create', () => {
expect(AuthModule).toBeDefined();
expect(authModule).toBeDefined();
});
it('should provide config', () => {
const config = authModule.get(PASSED_CONFIG);
expect(config).toEqual({ config: { authority: 'something' } });
});
it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = authModule.get(StsConfigLoader);
expect(configLoader instanceof StsConfigStaticLoader).toBe(true);
});
});
describe('StsConfigHttpLoader', () => {
let authModule: AuthModule;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
AuthModule.forRoot({
loader: {
provide: StsConfigLoader,
useFactory: () => new StsConfigHttpLoader(of({})),
},
}),
],
providers: [mockProvider(ConfigurationService)],
}).compileComponents();
authModule = TestBed.getImportByType(AuthModule);
});
it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = authModule.get(StsConfigLoader);
expect(configLoader instanceof StsConfigHttpLoader).toBe(true);
});
});
});

View File

@ -1,41 +0,0 @@
import {
type InjectionToken,
Injector,
ReflectiveInjector,
type Type,
} from '@outposts/injection-js';
import type { PassedInitialConfig } from './auth-config';
import type { Module } from './injection';
import { _provideAuth } from './provide-auth';
export interface AuthModuleOptions {
passedConfig: PassedInitialConfig;
parentInjector?: ReflectiveInjector;
}
export class AuthModule extends Injector {
passedConfig: PassedInitialConfig;
injector: ReflectiveInjector;
parentInjector?: Injector;
constructor(passedConfig?: PassedInitialConfig, parentInjector?: Injector) {
super();
this.passedConfig = passedConfig ?? {};
this.parentInjector = parentInjector;
this.injector = ReflectiveInjector.resolveAndCreate(
[..._provideAuth(this.passedConfig)],
this.parentInjector
);
}
static forRoot(passedConfig?: PassedInitialConfig): Module {
return (parentInjector?: Injector) =>
new AuthModule(passedConfig, parentInjector);
}
get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T): T;
get(token: any, notFoundValue?: any): any;
get(token: unknown, notFoundValue?: unknown): any {
return this.injector.get(token, notFoundValue);
}
}

131
src/features.ts Normal file
View File

@ -0,0 +1,131 @@
import type { HttpFeature } from '@ngify/http';
import type { Provider } from '@outposts/injection-js';
import { DOCUMENT } from './dom';
import { provideHttpClient } from './http';
import {
AbstractRouter,
VanillaHistoryRouter,
VanillaLocationRouter,
} from './router';
import { AbstractSecurityStorage } from './storage/abstract-security-storage';
import { DefaultLocalStorageService } from './storage/default-localstorage.service';
import { DefaultSessionStorageService } from './storage/default-sessionstorage.service';
import { PLATFORM_ID } from './utils/platform-provider/platform.provider';
/**
* A feature to be used with `provideAuth`.
*/
export interface AuthFeature {
ɵproviders: Provider[];
}
export interface BrowserPlatformFeatureOptions {
enabled?: boolean;
}
export function withBrowserPlatform({
enabled = true,
}: BrowserPlatformFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
{
provide: DOCUMENT,
useFactory: () => document,
},
{
provide: PLATFORM_ID,
useValue: 'browser',
},
]
: [],
};
}
export interface HttpClientFeatureOptions {
enabled?: boolean;
features?: HttpFeature[];
}
export function withHttpClient({
features,
enabled = true,
}: HttpClientFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled ? provideHttpClient(features) : [],
};
}
export type SecurityStorageType = 'session-storage' | 'local-storage';
export interface SecurityStorageFeatureOptions {
enabled?: boolean;
type?: SecurityStorageType;
}
export function withSecurityStorage({
enabled = true,
type = 'session-storage',
}: SecurityStorageFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
type === 'session-storage'
? {
provide: AbstractSecurityStorage,
useClass: DefaultLocalStorageService,
}
: {
provide: AbstractSecurityStorage,
useClass: DefaultSessionStorageService,
},
]
: [],
};
}
export type VanillaRouterType = 'location' | 'history';
export interface VanillaRouterFeatureOptions {
enabled?: boolean;
type?: VanillaRouterType;
}
export function withVanillaRouter({
enabled = true,
type = 'history',
}: VanillaRouterFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
type === 'location'
? {
provide: AbstractRouter,
useClass: VanillaLocationRouter,
}
: {
provide: AbstractRouter,
useClass: VanillaHistoryRouter,
},
]
: [],
};
}
export interface DefaultFeaturesOptions {
browserPlatform?: BrowserPlatformFeatureOptions;
securityStorage?: SecurityStorageFeatureOptions;
router?: VanillaRouterFeatureOptions;
httpClient?: HttpClientFeatureOptions;
}
export function withDefaultFeatures(
options: DefaultFeaturesOptions = {}
): AuthFeature[] {
return [
withBrowserPlatform(options.browserPlatform),
withSecurityStorage(options.securityStorage),
withHttpClient(options.httpClient),
withVanillaRouter(options.router),
].filter(Boolean) as AuthFeature[];
}

View File

@ -1,10 +1,15 @@
import { import {
// biome-ignore lint/nursery/noExportedImports: <explanation>
HttpClient as DefaultHttpClient,
type HttpBackend, type HttpBackend,
HttpClient,
type HttpFeature, type HttpFeature,
HttpFeatureKind, HttpFeatureKind,
// biome-ignore lint/nursery/noExportedImports: <explanation>
HttpHeaders,
type HttpInterceptor, type HttpInterceptor,
type HttpInterceptorFn, type HttpInterceptorFn,
type HttpRequest,
type HttpParams as NgifyHttpParams,
withInterceptors, withInterceptors,
withLegacyInterceptors, withLegacyInterceptors,
} from '@ngify/http'; } from '@ngify/http';
@ -13,8 +18,12 @@ import {
Optional, Optional,
type Provider, type Provider,
} from '@outposts/injection-js'; } from '@outposts/injection-js';
import type { Observable } from 'rxjs';
import type { ArrayOrNullableOne } from '../utils/types'; import type { ArrayOrNullableOne } from '../utils/types';
export { HttpParams, type HttpParamsOptions } from './params'; // biome-ignore lint/nursery/noExportedImports: <explanation>
import { HttpParams, type HttpParamsOptions } from './params';
export { HttpParams, HttpHeaders, type HttpParamsOptions, DefaultHttpClient };
export const HTTP_FEATURES = new InjectionToken<HttpFeature[]>('HTTP_FEATURES'); export const HTTP_FEATURES = new InjectionToken<HttpFeature[]>('HTTP_FEATURES');
@ -112,14 +121,29 @@ export function provideHttpClient(features: HttpFeature[] = []): Provider[] {
multi: true, multi: true,
}, },
{ {
provide: HttpClient, provide: HTTP_CLIENT,
useFactory: (features: ArrayOrNullableOne<HttpFeature>[]) => { useFactory: (features: ArrayOrNullableOne<HttpFeature>[]) => {
const normalizedFeatures = [features] const normalizedFeatures = [features]
.flat(Number.MAX_SAFE_INTEGER) .flat(Number.MAX_SAFE_INTEGER)
.filter(Boolean) as HttpFeature[]; .filter(Boolean) as HttpFeature[];
return new HttpClient(...normalizedFeatures); return new DefaultHttpClient(...normalizedFeatures);
}, },
deps: [HTTP_FEATURES], deps: [HTTP_FEATURES],
}, },
]; ];
} }
export type HttpClient = {
get<T>(
url: string,
options?: { headers?: HttpHeaders; params?: NgifyHttpParams }
): Observable<T>;
post<T>(
url: string,
body?: HttpRequest<any>['body'],
options?: { headers?: HttpHeaders; params?: NgifyHttpParams }
): Observable<T>;
};
export const HTTP_CLIENT = new InjectionToken<HttpClient>('HTTP_CLIENT');

View File

@ -4,7 +4,6 @@ export type { PassedInitialConfig } from './auth-config';
export * from './auth-options'; export * from './auth-options';
export * from './auth-state/auth-result'; export * from './auth-state/auth-result';
export * from './auth-state/auth-state'; export * from './auth-state/auth-state';
export * from './auth.module';
export * from './auto-login/auto-login-partial-routes.guard'; export * from './auto-login/auto-login-partial-routes.guard';
export * from './config/auth-well-known/auth-well-known-endpoints'; export * from './config/auth-well-known/auth-well-known-endpoints';
export * from './config/config.service'; export * from './config/config.service';
@ -31,3 +30,4 @@ export * from './validation/validation-result';
export * from './injection'; export * from './injection';
export * from './router'; export * from './router';
export * from './http'; export * from './http';
export * from './features';

View File

@ -1,11 +1,15 @@
import { TestBed } from '@/testing';
import { import {
type DefaultHttpTestingController,
HTTP_CLIENT_TEST_CONTROLLER, HTTP_CLIENT_TEST_CONTROLLER,
TestBed,
provideHttpClientTesting, provideHttpClientTesting,
} from '@/testing'; } from '@/testing';
import { HttpClient } from '@ngify/http'; import {
import type { HttpTestingController } from '@ngify/http/testing'; type DefaultHttpClient,
import { HTTP_INTERCEPTOR_FNS, HTTP_LEGACY_INTERCEPTORS } from 'oidc-client-rx'; HTTP_CLIENT,
HTTP_INTERCEPTOR_FNS,
HTTP_LEGACY_INTERCEPTORS,
} from 'oidc-client-rx';
import { ReplaySubject, firstValueFrom, share } from 'rxjs'; import { ReplaySubject, firstValueFrom, share } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
@ -16,9 +20,9 @@ import { AuthInterceptor, authInterceptor } from './auth.interceptor';
import { ClosestMatchingRouteService } from './closest-matching-route.service'; import { ClosestMatchingRouteService } from './closest-matching-route.service';
describe('AuthHttpInterceptor', () => { describe('AuthHttpInterceptor', () => {
let httpTestingController: HttpTestingController; let httpTestingController: DefaultHttpTestingController;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let httpClient: HttpClient; let httpClient: DefaultHttpClient;
let authStateService: AuthStateService; let authStateService: AuthStateService;
let closestMatchingRouteService: ClosestMatchingRouteService; let closestMatchingRouteService: ClosestMatchingRouteService;
@ -40,7 +44,7 @@ describe('AuthHttpInterceptor', () => {
], ],
}); });
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HTTP_CLIENT) as DefaultHttpClient;
httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER); httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
@ -72,7 +76,7 @@ describe('AuthHttpInterceptor', () => {
], ],
}); });
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HTTP_CLIENT) as DefaultHttpClient;
httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER); httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);

View File

@ -1,4 +1,3 @@
import { HttpClient } from '@ngify/http';
import type { Provider } from '@outposts/injection-js'; import type { Provider } from '@outposts/injection-js';
import { DataService } from './api/data.service'; import { DataService } from './api/data.service';
import { HttpBaseService } from './api/http-base.service'; import { HttpBaseService } from './api/http-base.service';
@ -24,6 +23,7 @@ import { StsConfigLoader } from './config/loader/config-loader';
import { ConfigValidationService } from './config/validation/config-validation.service'; import { ConfigValidationService } from './config/validation/config-validation.service';
import { DOCUMENT } from './dom'; import { DOCUMENT } from './dom';
import { JwkExtractor } from './extractors/jwk.extractor'; import { JwkExtractor } from './extractors/jwk.extractor';
import type { AuthFeature } from './features';
import { CodeFlowCallbackHandlerService } from './flows/callback-handling/code-flow-callback-handler.service'; import { CodeFlowCallbackHandlerService } from './flows/callback-handling/code-flow-callback-handler.service';
import { HistoryJwtKeysCallbackHandlerService } from './flows/callback-handling/history-jwt-keys-callback-handler.service'; import { HistoryJwtKeysCallbackHandlerService } from './flows/callback-handling/history-jwt-keys-callback-handler.service';
import { ImplicitFlowCallbackHandlerService } from './flows/callback-handling/implicit-flow-callback-handler.service'; import { ImplicitFlowCallbackHandlerService } from './flows/callback-handling/implicit-flow-callback-handler.service';
@ -62,10 +62,7 @@ import { UserService } from './user-data/user.service';
import { CryptoService } from './utils/crypto/crypto.service'; import { CryptoService } from './utils/crypto/crypto.service';
import { EqualityService } from './utils/equality/equality.service'; import { EqualityService } from './utils/equality/equality.service';
import { FlowHelper } from './utils/flowHelper/flow-helper.service'; import { FlowHelper } from './utils/flowHelper/flow-helper.service';
import { import { PlatformProvider } from './utils/platform-provider/platform.provider';
PLATFORM_ID,
PlatformProvider,
} from './utils/platform-provider/platform.provider';
import { RedirectService } from './utils/redirect/redirect.service'; import { RedirectService } from './utils/redirect/redirect.service';
import { TokenHelperService } from './utils/tokenHelper/token-helper.service'; import { TokenHelperService } from './utils/tokenHelper/token-helper.service';
import { CurrentUrlService } from './utils/url/current-url.service'; import { CurrentUrlService } from './utils/url/current-url.service';
@ -75,20 +72,17 @@ import { JwtWindowCryptoService } from './validation/jwt-window-crypto.service';
import { StateValidationService } from './validation/state-validation.service'; import { StateValidationService } from './validation/state-validation.service';
import { TokenValidationService } from './validation/token-validation.service'; import { TokenValidationService } from './validation/token-validation.service';
/**
* A feature to be used with `provideAuth`.
*/
export interface AuthFeature {
ɵproviders: Provider[];
}
export function provideAuth( export function provideAuth(
passedConfig: PassedInitialConfig, passedConfig: PassedInitialConfig,
...features: AuthFeature[] ...features: (AuthFeature | AuthFeature[])[]
): Provider[] { ): Provider[] {
const providers = _provideAuth(passedConfig); const providers = _provideAuth(passedConfig);
for (const feature of features) { const normailizedFeatures = features
.flat(Number.MAX_SAFE_INTEGER)
.filter(Boolean) as AuthFeature[];
for (const feature of normailizedFeatures) {
providers.push(...feature.ɵproviders); providers.push(...feature.ɵproviders);
} }
@ -97,15 +91,6 @@ export function provideAuth(
export function _provideAuth(passedConfig: PassedInitialConfig): Provider[] { export function _provideAuth(passedConfig: PassedInitialConfig): Provider[] {
return [ return [
{
provide: DOCUMENT,
useFactory: () => document,
},
HttpClient,
{
provide: PLATFORM_ID,
useValue: 'browser',
},
// Make the PASSED_CONFIG available through injection // Make the PASSED_CONFIG available through injection
{ provide: PASSED_CONFIG, useValue: passedConfig }, { provide: PASSED_CONFIG, useValue: passedConfig },
// Create the loader: Either the one getting passed or a static one // Create the loader: Either the one getting passed or a static one

View File

@ -1,3 +1,6 @@
import { inject } from '@outposts/injection-js';
import { DOCUMENT } from 'src/dom';
export type RouteData = { export type RouteData = {
[key: string | symbol]: any; [key: string | symbol]: any;
}; };
@ -26,3 +29,55 @@ export abstract class AbstractRouter<
abstract getCurrentNavigation(): NAVIGATION; abstract getCurrentNavigation(): NAVIGATION;
} }
export class VanillaLocationRouter extends AbstractRouter {
private document = inject(DOCUMENT);
private get location(): Location {
const location = this.document.defaultView?.window?.location;
if (!location) {
throw new Error('current document do not support Location API');
}
return location;
}
navigateByUrl(url: string): void {
this.location.href = url;
}
getCurrentNavigation() {
return {
extractedUrl: `${this.location.pathname}${this.location.search}${this.location.hash}`,
};
}
}
export class VanillaHistoryRouter extends AbstractRouter<string> {
private document = inject(DOCUMENT);
private get history(): History {
const history = this.document.defaultView?.window?.history;
if (!history) {
throw new Error('current document do not support History API');
}
return history;
}
private get location(): Location {
const location = this.document.defaultView?.window?.location;
if (!location) {
throw new Error('current document do not support Location API');
}
return location;
}
navigateByUrl(url: string): void {
this.history.pushState({}, '', url);
}
getCurrentNavigation() {
return {
extractedUrl: `${this.location.pathname}${this.location.search}${this.location.hash}`,
};
}
}

View File

@ -2,6 +2,8 @@ import { HttpClientTestingBackend } from '@ngify/http/testing';
import { InjectionToken, type Provider } from '@outposts/injection-js'; import { InjectionToken, type Provider } from '@outposts/injection-js';
import { HTTP_BACKEND, provideHttpClient } from 'oidc-client-rx'; import { HTTP_BACKEND, provideHttpClient } from 'oidc-client-rx';
export { HttpTestingController as DefaultHttpTestingController } from '@ngify/http/testing';
export const HTTP_CLIENT_TEST_CONTROLLER = export const HTTP_CLIENT_TEST_CONTROLLER =
new InjectionToken<HttpClientTestingBackend>('HTTP_CLIENT_TEST_CONTROLLER'); new InjectionToken<HttpClientTestingBackend>('HTTP_CLIENT_TEST_CONTROLLER');

View File

@ -6,4 +6,8 @@ export {
} from './spy'; } from './spy';
export { createRetriableStream } from './create-retriable-stream.helper'; export { createRetriableStream } from './create-retriable-stream.helper';
export { MockRouter, mockRouterProvider } from './router'; export { MockRouter, mockRouterProvider } from './router';
export { provideHttpClientTesting, HTTP_CLIENT_TEST_CONTROLLER } from './http'; export {
provideHttpClientTesting,
HTTP_CLIENT_TEST_CONTROLLER,
DefaultHttpTestingController,
} from './http';