如何测试NestJS拦截器的错误捕获/抛出?

问题描述 投票:0回答:1

我正在将一个普通的 Express 应用程序移植/重写到 NestJS。该应用程序是将请求转换/传递到端点的中间件。我有一个相当简单的 Express 拦截器,它在传出请求上设置身份验证令牌标头,但如果失败/过期,它将使用更新的身份验证令牌重试。

@Injectable()
export class TokenInterceptor implements NestInterceptor {
  private readonly authUrl = 'https://example.come/tokens/OAuth';
  private token = '';

  constructor(private readonly http: HttpService) {
    //Get the token when the app starts up
    this.fetchToken().subscribe((token) => {
      this.token = token;
      this.http.axiosRef.defaults.headers.common.Authorization = this.token;
    });
  }

  public intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
    this.http.axiosRef.defaults.headers.common.Authorization = this.token;
    
    return next.handle().pipe(
      catchError(async (error: AxiosError) => {
        if (error.config) {
          //If we have an auth error, we probably just need to re-fetch the token
          if (error.response?.status === HttpStatus.UNAUTHORIZED && error.config.url !== this.authUrl) {
            //without second check, we can loop forever
            this.token = await lastValueFrom(this.fetchToken());
            this.http.axiosRef.defaults.headers.common.Authorization = this.token; //reset default to valid
            return of(error.request); //retry request
          }
        }

        //Some other error type, so we want to throw that back
        return throwError(() => {
          const msg = error.message;
          const code = error.status ?? 500;
          return new HttpException(msg, code, { cause: error });
        });
      }),
    );
  }

  private fetchToken(): Observable<string> {
    return this.http
      .post<ISharepointTokenResponseData>(
        this.authUrl,
        {/*special params here*/},
      )
      .pipe(map((res) => `Bearer ${res.data.access_token}`));
  }
}

我如何正确地为所有这些编写单元测试?这是我到目前为止所得到的,对不起作用的测试添加的评论

describe('TokenInterceptor', () => {
  let module: TestingModule;
  let interceptor: SharepointTokenInterceptor;
  const httpService = mockDeep<HttpService>();

  /**
   * @description Normally this would be in the `beforeEach` but we need to put a jest spy on http requests,
   * so Call this INSIDE each test AFTER setting up HTTP spies.
   * It has to be this way because the class constructor kicks off an initial HTTP request, which would happen before our test could run */
  const createTestingModule = async () => {
    module = await Test.createTestingModule({
      providers: [
        SharepointTokenInterceptor,
        {
          provide: HttpService,
          useValue: httpService,
        },
      ],
    }).compile();
    interceptor = module.get(SharepointTokenInterceptor);
  };

  const provideMockToken = (mockToken: string) => {
    jest.spyOn(httpService, 'post').mockReturnValueOnce(
      of({
        data: { access_token: mockToken },
      } as AxiosResponse<ISharepointTokenResponseData>),
    );
  };

  it('should fetch an auth token initially', async () => {
    const mockToken = 'my-fake-token';
    provideMockToken(mockToken);
    await createTestingModule();

    //Make sure the correct HTTP calls are made
    expect(httpService.post).toHaveBeenCalledTimes(1);
    expect(httpService.post).toHaveBeenCalledWith(
      'https://example.come/tokens/OAuth',
      {/*special params here*/},
    );

    //Make sure the value returned from the HTTP calls is properly set on the properties we expect
    // eslint-disable-next-line @typescript-eslint/dot-notation -- we are being naughty and accessing a private class member so we have to use string indexing to cheat!
    expect(interceptor['token']).toEqual(`Bearer ${mockToken}`);
    expect(httpService.axiosRef.defaults.headers.common.Authorization).toEqual(`Bearer ${mockToken}`);
  });

  it('should add auth headers to outgoing requests when we have an auth token', async () => {
    provideMockToken('originalToken');
    await createTestingModule();

    const executionCtxMock = mockDeep<ExecutionContext>();
    const nextMock: CallHandler = {
      handle: jest.fn(() => of()),
    };

    //Manually force a new token - this should never happen but this is just insurance to know that we do set the token on each outgoing request
    // eslint-disable-next-line @typescript-eslint/dot-notation -- we have to use string indexing to cheat!
    interceptor['token'] = `Bearer new-token`;
    interceptor.intercept(executionCtxMock, nextMock);

    expect(httpService.axiosRef.defaults.headers.common.Authorization).toEqual(`Bearer new-token`);
    expect(nextMock.handle).toHaveBeenCalledTimes(1);
    expect(nextMock.handle).toHaveBeenCalledWith();
  });

  describe('Error Handling', () => {
    beforeEach(async () => {
      provideMockToken('example-token');
      await createTestingModule();
    });

    //==================================================================
    //This test fails because it returns success in the observable, even though it's using throwError!
    //I am not sure what is going on here.
    //Is my test wrong, or am I re-throwing an error wrong in the interceptor?
    it('should pass the error through for non-401 errors', (done: jest.DoneCallback) => {
      const executionCtxMock = mockDeep<ExecutionContext>();
      const nextMock: CallHandler = {
        handle: () =>
          throwError(
            () =>
              new AxiosError('you screwed up!', '403', {
                //@ts-expect-error -- we don't need the headers for this test, so this is OK
                headers: {},
                url: 'https://ytmnd.com',
              }),
          ),
      };

      interceptor.intercept(executionCtxMock, nextMock).subscribe({
        next: (res) => {
          console.log(res); //This logs out the exception even though this is in the success block!
          //This is not supposed to succeed!
          expect(false).toEqual(true);
          done();
        },
        error: (err: unknown) => {
          expect(err).toEqual(new BadRequestException('you screwed up!'));
          done();
        },
      });
    });
    
    xit('should refetch the auth token when we get a 401 "unauthorized" response and add that new token to outgoing requests', (done: jest.DoneCallback) => {
      //Haven't gotten to this one yet     
    });

    xit('should NOT refetch the auth token when we get a 401 "unauthorized" response and the URL is the URL for making an auth request', (done: jest.DoneCallback) => {
      //Haven't gotten to this one yet
    });
  });
});
nestjs nestjs-interceptor
1个回答
0
投票

所以显然我只需要直接返回错误而不是返回

throwError
,因为那将是一个内部/嵌套的可观察值

public intercept(_context: ExecutionContext, next: CallHandler): Observable<Request | HttpException> {
  this.http.axiosRef.defaults.headers.common.Authorization = this.token;
  return next.handle().pipe(
    catchError(async (error: AxiosError) => {
      if (error.config) {
        //If we have an auth error, we probably just need to re-fetch the token
        if (error.response?.status === HttpStatus.UNAUTHORIZED && error.config.url !== this.authUrl) {
          //without second check, we can loop forever
          this.token = await lastValueFrom(this.fetchToken());
          this.http.axiosRef.defaults.headers.common.Authorization = this.token; //reset default to valid
          return error.request as Request; //retry request
        }
      }

      //Some other error type, so we want to throw that back
      const msg = error.message;
      const code = error.status ?? 500;
      return new HttpException(msg, code, { cause: error });
    }),
  );
}

然后我的测试需要返回该值并期望它是一个错误

it('should pass the error through for non-401 errors', async () => {
  provideMockTokens('flavor-blasted-goldfish');
  await createTestingModule();

  const mockError = new AxiosError(
    'You have been very bad to request such a thing like this!',
    'idk what this is',
    //@ts-expect-error - we don't care about the headers being incorrect here
    { url: expectedAuthUrl, headers: {} },
    {},
    { status: HttpStatus.BAD_REQUEST },
  );

  const expectedError = new HttpException(mockError.message, HttpStatus.BAD_REQUEST, { cause: mockError });

  const executionCtxMock = mockDeep<ExecutionContext>();
  const nextMock: CallHandler = {
    handle: jest.fn().mockReturnValueOnce(throwError(() => mockError)),
  };

  const val = await lastValueFrom(interceptor.intercept(executionCtxMock, nextMock));

  expect(val).toEqual(expectedError);
  expect(nextMock.handle).toHaveBeenCalledTimes(1);
});

我不完全理解这一点,因为我希望当我返回错误或

throwError
时,可观察对象会失败,但它会成功并以错误作为返回结果。好吧,无论如何,只要在 Nest 内一切正常!

© www.soinside.com 2019 - 2024. All rights reserved.