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

View File

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

View File

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

View File

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