fix: fix interceptors

This commit is contained in:
master 2025-02-03 23:53:49 +08:00
parent eacbbb2815
commit 26a06fdbf0
7 changed files with 291 additions and 68 deletions

View File

@ -1,8 +1,10 @@
import { TestBed } from '@/testing';
import { provideHttpClientTesting } from '@/testing/http';
import {
HTTP_CLIENT_TEST_CONTROLLER,
provideHttpClientTesting,
} from '@/testing/http';
import { HttpHeaders } from '@ngify/http';
import { HttpTestingController } from '@ngify/http/testing';
import { provideHttpClient, withInterceptorsFromDi } from 'oidc-client-rx';
import type { HttpTestingController } from '@ngify/http/testing';
import { firstValueFrom } from 'rxjs';
import { DataService } from './data.service';
import { HttpBaseService } from './http-base.service';
@ -14,15 +16,10 @@ describe('Data Service', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DataService,
HttpBaseService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
providers: [DataService, HttpBaseService, provideHttpClientTesting()],
});
dataService = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
httpMock = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
});
it('should create', () => {

View File

@ -1,17 +1,121 @@
import type { HttpFeature, HttpInterceptor } from '@ngify/http';
import { InjectionToken } from 'injection-js';
import {
type HttpBackend,
HttpClient,
type HttpFeature,
HttpFeatureKind,
type HttpInterceptor,
type HttpInterceptorFn,
withInterceptors,
withLegacyInterceptors,
} from '@ngify/http';
import { InjectionToken, Optional, type Provider } from 'injection-js';
import type { ArrayOrNullableOne } from '../utils/types';
export { HttpParams, HttpParamsOptions } from './params';
export const HTTP_INTERCEPTORS = new InjectionToken<readonly HttpInterceptor[]>(
'HTTP_INTERCEPTORS'
export const HTTP_FEATURES = new InjectionToken<HttpFeature[]>('HTTP_FEATURES');
export const HTTP_INTERCEPTOR_FNS = new InjectionToken<HttpInterceptorFn[]>(
'HTTP_INTERCEPTOR_FNS'
);
export function provideHttpClient() {
// todo
throw new Error('todo!');
}
export const HTTP_LEGACY_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>(
'HTTP_LEGACY_INTERCEPTORS'
);
export function withInterceptorsFromDi(): HttpFeature {
// todo
throw new Error('todo!');
export const HTTP_BACKEND = new InjectionToken<HttpBackend>('HTTP_BACKEND');
export const HTTP_XSRF_PROTECTION = new InjectionToken<HttpInterceptorFn>(
'HTTP_XSRF_PROTECTION'
);
export function provideHttpClient(features: HttpFeature[] = []): Provider[] {
return [
{
provide: HTTP_INTERCEPTOR_FNS,
multi: true,
useValue: [],
},
{
provide: HTTP_LEGACY_INTERCEPTORS,
multi: true,
useValue: [],
},
{
provide: HTTP_FEATURES,
useFactory: (
interceptors: ArrayOrNullableOne<HttpInterceptorFn>[]
): HttpFeature[] => {
const normalizedInterceptors = [interceptors]
.flat(Number.MAX_SAFE_INTEGER)
.filter(Boolean) as HttpInterceptorFn[];
return normalizedInterceptors.length
? [withInterceptors(normalizedInterceptors)]
: [];
},
multi: true,
deps: [HTTP_INTERCEPTOR_FNS],
},
{
provide: HTTP_FEATURES,
useFactory: (
interceptors: ArrayOrNullableOne<HttpInterceptor>[]
): HttpFeature[] => {
const normalizedInterceptors = [interceptors]
.flat(Number.MAX_SAFE_INTEGER)
.filter(Boolean) as HttpInterceptor[];
return normalizedInterceptors.length
? [withLegacyInterceptors(normalizedInterceptors)]
: [];
},
multi: true,
deps: [HTTP_LEGACY_INTERCEPTORS],
},
{
provide: HTTP_FEATURES,
useFactory: (backend: HttpBackend | null | undefined): HttpFeature[] => {
return backend
? [
{
kind: HttpFeatureKind.Backend,
value: backend,
},
]
: [];
},
multi: true,
deps: [[new Optional(), HTTP_BACKEND]],
},
{
provide: HTTP_FEATURES,
useFactory: (
interceptor: HttpInterceptorFn | null | undefined
): HttpFeature[] => {
return interceptor
? [
{
kind: HttpFeatureKind.XsrfProtection,
value: interceptor,
},
]
: [];
},
multi: true,
deps: [[new Optional(), HTTP_XSRF_PROTECTION]],
},
{
provide: HTTP_FEATURES,
useValue: features,
multi: true,
},
{
provide: HttpClient,
useFactory: (features: ArrayOrNullableOne<HttpFeature>[]) => {
const normalizedFeatures = [features]
.flat(Number.MAX_SAFE_INTEGER)
.filter(Boolean) as HttpFeature[];
return new HttpClient(...normalizedFeatures);
},
deps: [HTTP_FEATURES],
},
];
}

View File

@ -1,16 +1,12 @@
import { TestBed } from '@/testing';
import {
HTTP_INTERCEPTORS,
HttpClient,
provideHttpClient,
withInterceptors,
withInterceptorsFromDi,
} from '@ngify/http';
import {
HttpTestingController,
HTTP_CLIENT_TEST_CONTROLLER,
provideHttpClientTesting,
} from '@ngify/http/testing';
import { firstValueFrom } from 'rxjs';
} from '@/testing';
import { HttpClient } from '@ngify/http';
import type { HttpTestingController } from '@ngify/http/testing';
import { HTTP_INTERCEPTOR_FNS, HTTP_LEGACY_INTERCEPTORS } from 'oidc-client-rx';
import { ReplaySubject, firstValueFrom, share } from 'rxjs';
import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service';
@ -33,20 +29,19 @@ describe('AuthHttpInterceptor', () => {
providers: [
ClosestMatchingRouteService,
{
provide: HTTP_INTERCEPTORS,
provide: HTTP_LEGACY_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true,
},
mockProvider(AuthStateService),
mockProvider(LoggerService),
mockProvider(ConfigurationService),
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
});
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
configurationService = TestBed.inject(ConfigurationService);
authStateService = TestBed.inject(AuthStateService);
closestMatchingRouteService = TestBed.inject(ClosestMatchingRouteService);
@ -64,8 +59,12 @@ describe('AuthHttpInterceptor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: HTTP_INTERCEPTOR_FNS,
useFactory: authInterceptor,
multi: true,
},
ClosestMatchingRouteService,
provideHttpClient(withInterceptors([authInterceptor()])),
provideHttpClientTesting(),
mockProvider(AuthStateService),
mockProvider(LoggerService),
@ -74,7 +73,7 @@ describe('AuthHttpInterceptor', () => {
});
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
httpTestingController = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
configurationService = TestBed.inject(ConfigurationService);
authStateService = TestBed.inject(AuthStateService);
closestMatchingRouteService = TestBed.inject(ClosestMatchingRouteService);
@ -106,14 +105,27 @@ describe('AuthHttpInterceptor', () => {
true
);
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
httpRequest.flush('something');
expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -132,14 +144,26 @@ describe('AuthHttpInterceptor', () => {
true
);
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -159,15 +183,27 @@ describe('AuthHttpInterceptor', () => {
vi.spyOn(authStateService, 'getAccessToken').mockReturnValue(
'thisIsAToken'
);
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -185,14 +221,26 @@ describe('AuthHttpInterceptor', () => {
true
);
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -211,14 +259,27 @@ describe('AuthHttpInterceptor', () => {
);
vi.spyOn(authStateService, 'getAccessToken').mockReturnValue('');
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -229,14 +290,26 @@ describe('AuthHttpInterceptor', () => {
false
);
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -260,14 +333,25 @@ describe('AuthHttpInterceptor', () => {
matchingConfig: null,
});
const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
expect(httpRequest.request.headers.has('Authorization')).toEqual(false);
httpRequest.flush('something');
const response = await firstValueFrom(test$);
expect(response).toBeTruthy();
httpTestingController.verify();
});
@ -286,11 +370,25 @@ describe('AuthHttpInterceptor', () => {
true
);
let response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy();
const test$ = httpClient.get(actionUrl).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
const test2$ = httpClient.get(actionUrl2).pipe(
share({
connector: () => new ReplaySubject(),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
response = await firstValueFrom(httpClient.get(actionUrl2));
expect(response).toBeTruthy();
test$.subscribe();
test2$.subscribe();
const httpRequest = httpTestingController.expectOne(actionUrl);
@ -302,6 +400,14 @@ describe('AuthHttpInterceptor', () => {
httpRequest.flush('something');
httpRequest2.flush('something');
const [response, response2] = await Promise.all([
firstValueFrom(test$),
firstValueFrom(test2$),
]);
expect(response).toBeTruthy();
expect(response2).toBeTruthy();
httpTestingController.verify();
});
}

View File

@ -40,13 +40,14 @@ export class AuthInterceptor implements HttpInterceptor {
}
export function authInterceptor(): HttpInterceptorFn {
const deps = {
configurationService: inject(ConfigurationService),
authStateService: inject(AuthStateService),
closestMatchingRouteService: inject(ClosestMatchingRouteService),
loggerService: inject(LoggerService),
};
return (req, next) => {
return interceptRequest(req, next, {
configurationService: inject(ConfigurationService),
authStateService: inject(AuthStateService),
closestMatchingRouteService: inject(ClosestMatchingRouteService),
loggerService: inject(LoggerService),
});
return interceptRequest(req, next, deps);
};
}

View File

@ -1,9 +1,21 @@
import { HttpFeatureKind } from '@ngify/http';
import { HttpClientTestingBackend } from '@ngify/http/testing';
import { InjectionToken, type Provider } from 'injection-js';
import { HTTP_BACKEND, provideHttpClient } from 'oidc-client-rx';
export function provideHttpClientTesting() {
return {
provide: HttpFeatureKind.Backend,
useClass: HttpClientTestingBackend,
};
export const HTTP_CLIENT_TEST_CONTROLLER =
new InjectionToken<HttpClientTestingBackend>('HTTP_CLIENT_TEST_CONTROLLER');
export function provideHttpClientTesting(): Provider[] {
const backend = new HttpClientTestingBackend();
return [
{
provide: HTTP_CLIENT_TEST_CONTROLLER,
useValue: backend,
},
{
provide: HTTP_BACKEND,
useValue: backend,
},
...provideHttpClient(),
];
}

View File

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

3
src/utils/types/index.ts Normal file
View File

@ -0,0 +1,3 @@
export type ArrayOrOne<T> = T[] | T;
export type ArrayOrNullableOne<T> = T[] | T | null | undefined;