[Nest.js] e2e test에서 Web3AuthGuard 오버라이드 하기

Donghun Seol·2023년 5월 6일
0

0. 배경

작성하고 있는 api에서 Get /users/my 와 같은 api 호출은 web3AuthGuard를 거쳐서 인증된 사용자에게만 정상적인 응답을 돌려준다.

문제는 테스트 환경에서 로그인을 구현하기가 복잡하다.
일반적인 로그인이라면 id/pw를 입력으로 받아 jwt토큰을 반환받아서 토큰을 헤더에 삽입해서 사용하면 되지만, cds는 web3 로그인이 구현되어 있다. 논스값을 받아오고, 캐시를 설정하고, 메타마스크를 활용해 서명값을 전송하는 로직이 있는데, 이런 전체 과정을 테스트로 구현하기는 (내 기준에선) 매우 어려웠다.

1. 아이디어

e2e 테스트에서 @Cron() 서비스 중지시키기 포스트에서 했던 것 처럼 가드를 아예 오버라이드 해버리는 방법이 떠올랐다.

처음엔 나이브하게 생각했고, 항상 그래왔듯 실패했다. 가드를 오버라이드 해서 빈 객체로 만들어버리면 의도대로 성공할 것이라 생각했는데, 이렇게 하면 가드와 의존관계에 있는 모듈들에서 에러가 발생하므로 기대했던 200 응답 대신에, 500 응답이 나왔다.

따라서 가드의 내부 인터페이스를 유지하면서 내부를 모킹하는 방법이 필요했다. 또한 Web3AuthGuard는 인증 성공시 req객체안에 인증된 이더리움 주소를 삽입해야 하므로 모킹가드에서도 똑같은 작업해줬다.

2. 해결

내가 작성한 통합테스트는 매번 아래와 같이 moduleFixture에서 정의된 app객체를 사용한다.
따라서 .overrideGuard() 메서드로 가드를 오버라이드 해줄 수 있다. nest.js의 가드는 canActivate()를 포함해야 하므로, 해당 메서드를 오버라이드 해주는 객체안에 포함시키고, 그 안에서 req객체에 verifiedAddress 값을 포함시키는 작업을 수행해줬다.

이렇게 구성된 app객체로 들어오는 요청은 validAddress로 Web3AuthGuard 인증을 통과한 형태로 전달된다. 따라서 Get /users/my와 같이 인증이 필요한 api를 항상 인증이 된 상태로 테스트 할 수 있다.

// app.e2e-spec.ts
const validAddress = '0x746d6375e82ba702785df6d5ec880da2480c16d7';

const moduleFixture: TestingModule = await Test.createTestingModule({
        imports: [AppModule],
      })
        .overrideProvider(PriceFeedService)
        .useValue({})
        .overrideGuard(Web3AuthGuard)
        .useValue({
          canActivate: (context: ExecutionContext) => {
            const request = context.switchToHttp().getRequest();
            request['user'] = { validAddress };
            console.log('overridden web3 auth guard called');
            return true;
          },
        })
        .compile();

3. 또 버그?

성공했다고 생각하고 계속 진행했는데 버그를 발견했다.

원래의 api는 req.user 안에 verifiedAddress라는 키를 활용해서 주소값을 저장했는데, 위에서 Web3AuthGuard를 오버라이딩 할때 validAddress라고 변수명을 짓는 바람에 실제 쿼리에 전달되는 address값이 undefined가 되었다.

게다가 일부 api는 undefined를 입력으로 받더라도 200코드를 응답으로 돌려줬고, 딱 하나의 api만 sql Error 에러를 뱉어줬기 때문에 원인을 찾기 더더욱 어려웠다. ORM단에서 발생한 오류인줄 착각하고 계속 그쪽만 디버깅해보고 있었다.

이런 휴먼에러를 근본적으로 예방하기 위해선 컨트롤러단에서 @Req 객체에 있는 값을 받아 쿼리핸들러에 전달하기 전에 입력값을 검증해야 한다. 커스텀 파라미터 데커레이터를 활용하는 것이 가장 좋은 방법이라고 생각해서 구현해 봤다.

현재의 코드는 아래와 같다. 디버깅을 위해 verifiedAddress가 undefined인지 체크하는 보호절을 삽입했다. 이 로직을 파라미터 데커레이터로 바꿔서 Req객체 전체를 받아오는게 아니라 verfiedAddress만 검증 후 받아오는 방식으로 변경할 필요가 있다.

//users.controller.ts

  @UseGuards(Web3AuthGuard)
  @Get('my/transactions')
  async getMyTransactions(
    @Req() req: Request & { user: { verifiedAddress: string } },
  ) {
    const { verifiedAddress } = req.user;
    if (!verifiedAddress)
      throw new Error(`verfed Address should not empty ${verifiedAddress}`);
    return await this.queryBus.execute(
      new GetUserTransactionsQuery(verifiedAddress),
    );
  }

아래의 코드 작성엔 ChatGPT를 활용했다.

전달한 프롬프트는 다음과 같다.

I want write custom parameter decorator for my nest.js application
this decorator will parse request and extract only target value which is in req object named verified address. and i want this decorator throw error if target value is null of undefined

이리저리 레퍼런스를 찾아 돌아다녔다면 구현에 10분은 넘게 걸렸을 것 같은데, 순식간에 원하는 코드를 얻었고, 약간만 수정한 뒤 적용해보니 의도한대로 잘 작동했다. 😎

먼저 파라미터 테커레이터를 정의한다.

//verified-address.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const VerifiedAddress = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const verifiedAddress = request?.user?.verifiedAddress;

    if (verifiedAddress === null || verifiedAddress === undefined) {
      throw new Error('Verified address is missing.');
    }

    return verifiedAddress;
  },
);

정의된 데커레이터를 컨트롤러 메서드에 적용한다.
관심사 분리가 적용되어 코드가 간결하고 가독성 좋게 개선되었다.

//users.controller.ts

@UseGuards(Web3AuthGuard)
@Get('my/transactions')
async getMyTransactions(@VerifiedAddress() verifiedAddress: string) {
  return await this.queryBus.execute(
    new GetUserTransactionsQuery(verifiedAddress),
  );
}
profile
I'm going from failure to failure without losing enthusiasm

3개의 댓글

comment-user-thumbnail
2023년 5월 12일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기