deno
oak
testing
token
error log

에러로그: oak testing에서 get과 set은 서로 관련이 없습니다.

Deno oak에 테스트 코드를 작성하면서 cookie 설정이 특이하다는 점을 발견했습니다.

deno oak의 testing에서 set과 get은 서로 무관합니다. createMockContext으로 만든 context(이하 ctx)는 ctx.cookies.set으로 넣었다고 ctx.cookies.get으로 꺼낼 수 있는 것은 아닙니다. 모두

미들웨어 컨트롤러

따라서 일반적인 흐름은 다음과 같습니다:

  1. 클라이언트가 요청을 보냄.
  2. 요청이 도달하면 미들웨어가 실행되어 전처리 작업 수행.
  3. 미들웨어는 요청을 검사하고 조작한 후, 컨트롤러에게 제어를 전달.
  4. 컨트롤러는 요청을 처리하고 필요한 작업을 수행.
  5. 컨트롤러는 응답을 생성하여 클라이언트에게 반환.
  6. 응답이 도달하면 미들웨어가 실행되어 후처리 작업 수행 및 응답을 클라이언트에게 전달.

ChatGPT에게 질문하고 얻은 답변입니다.

토큰 갱신 프론트엔드

토큰 갱신이 필요하다는 응답을 받으면 프론트엔드는 어떻게 처리하는지 궁금해졌습니다.

axios interceptors와 refresh token을 활용한 jwt 토큰 관리 - HyunGyu-Kim

잘 다룬 블로그를 발견했습니다.

Deno oak testing

Deno oak를 테스트하는 방법을 찾고 있었습니다.

cookie를 서버에서 설정하고 클라이언트가 요청 보낼 때마다 확인해야 하는데 이것을 어떻게 구현하는지 찾아보고 있었습니다.

fix: mocked contexts provide the cookies property #422 - oakserver / oak

Deno.test('should set cookie from mock context', async () => {
  const hash = 'hash1234';
  const ctx = createMockContext();

  await ctx.cookies.set('sessionID', hash, { httpOnly: true });

  assertEquals(
    [...ctx.response.headers],
    [['set-cookie', `sessionID=${hash}; path=/; httponly`]]
  );
});

Deno.test('should get cookie from mock context', async () => {
  const hash = 'hash1234';
  const ctx = createMockContext({
    headers: [['cookie', `sessionID=${hash};`]],
  });

  assertEquals(await ctx.cookies.get('sessionID'), hash);
});

테스트 코드가 더럽지만 일단 어떻게 작성해야 하는지 알 수 있습니다.

안풀리는 미스테리: Cookies 설정 불가

보통 토큰은 2개로 테스트합니다. 하지만 이것에 대한 테스트 코드가 별로 없는 것도 의외입니다.

문제: 구현은 통과하지만 테스트코드는 실패합니다.

import type { Context, Middleware } from '../deps.ts';
import Token from '../util/token.ts';

const token = Token.getInstance();

const authMiddleware: Middleware = async (
  { request, response, cookies },
  next
) => {
  const accessToken = request.headers.get('Authorization');
  if (accessToken) {
    await next();
  } else {
    const refreshToken = await cookies.get('user');
    if (!refreshToken) {
      response.status = 400;
      response.body = {
        msg: '로그아웃 되었습니다.',
      };
    } else {
      response.status = 401;
      response.body = {
        msg: '토큰이 만료되었습니다.',
        accessToken: token.refreshAccessToken(refreshToken),
      };
    }
  }
};

export { authMiddleware };

이것에 대한 테스트코드를 작성하고 있는데 문제가 생겼습니다.

Deno.test('access token이 만료 refresh token은 유효',
  async () => {
    const ctx = testing.createMockContext({
      headers: [['Authorization', '']],
    });
    const refreshCookie = 'refreshToken';
    const next = testing.createMockNext();

    await ctx.cookies.set('user', refreshCookie);
    await authMiddleware(ctx, next);

    assertEquals(
      [...ctx.response.headers],
      [['set-cookie', `user=${refreshCookie}; path=/; httponly`]]
    );

    await authMiddleware(ctx, next);

    console.log(await ctx.cookies.get('user'), ctx.response.headers); // undefined
    assert(ctx.response.status === 401, '401');

    const configPromise = new
  }
);

cookiesset해도 get할 수 없습니다.

시도: ChatGPT 질문

Deno.test('access token이 만료 refresh token은 유효', async () => {
  const ctx = testing.createMockContext({
    headers: [['Authorization', '']],
  });
  const refreshCookie = 'refreshToken';
  const next = testing.createMockNext();

  await authMiddleware(ctx, next); // authMiddleware를 호출하기 전에 쿠키를 설정하지 않습니다.

  // 쿠키 설정 후에 await를 사용하여 Promise가 완료될 때까지 기다립니다.
  await ctx.cookies.set('user', refreshCookie);

  await authMiddleware(ctx, next);

  assertEquals(
    [...ctx.response.headers],
    [['set-cookie', `user=${refreshCookie}; path=/; httponly`]]
  );
  assert(ctx.response.status === 401, '401');
});

표본이 너무 작아서 GPT로 해결할 수 없었습니다.

promise 활용

Deno.test('access token이 만료 refresh token은 유효', async () => {
  const refreshCookie = 'refreshToken';
  const ctx = testing.createMockContext({
    headers: [['Authorization', '']],
  });
  const next = testing.createMockNext();

  await ctx.cookies.set('user', refreshCookie).then(() => {
    authMiddleware(ctx, next);
  });

  assertEquals(
    [...ctx.response.headers],
    [['set-cookie', `user=${refreshCookie}; path=/; httponly`]]
  );

  assert(ctx.response.status === 401, '401');
  assertEquals(
    ctx.response.body,
    {
      msg: '토큰이 만료되었습니다.',
      accessToken: async () => ({ accessToken: null, success: false }),
    },
    'access token 갱신 응답'
  );
});

순서에 맞게 실행했지만 그래도 테스트를 통과하지 않았습니다.

이 테스트 코드를 보면 확실하게 동작합니다. 하지만 set의 동작방식이 다릅니다.

해결: get과 set은 서로 읽고 쓰는 관계가 아닙니다.

Deno.test('should get cookie from mock context', async () => {
  const hash = 'hash1234';
  const ctx = createMockContext({
    headers: [['cookie', `sessionID=${hash};`]],
  });

  assertEquals(await ctx.cookies.get('sessionID'), hash);
});

github에서 이 예시를 보니까 cookiesset하는 동작이 다릅니다. 즉 set을 하면 get으로 접근이 가능한 것이 아니었습니다.

fix: mocked contexts provide the cookies property #422 - oakserver / oak

Deno.test('access token이 만료 refresh token은 유효',
  async () => {
    const refreshCookie = 'refreshToken';
    const ctx = testing.createMockContext({
      headers: [
        ['Authorization', ''],
        ['cookie', `user=${refreshCookie}`],
      ],
    });
    const next = testing.createMockNext();

    await authMiddleware(ctx, next);

    assert(ctx.response.status === 401, '401');
    assertEquals(
      ctx.response.body,
      {
        msg: '토큰이 만료되었습니다.',
        accessToken: Promise.resolve({ accessToken: null, success: false }),
      },
      'access token 갱신 응답'
    );
  },
});

결국 이렇게 해서 문제를 해결했습니다. 상당히 특이하고 시간을 많이 낭비하면서 겨우 힘을게 테스트를 구현했습니다.

학습: 초기 라이브러리는 손 볼 곳이 많습니다.

set을 하면 당연히 get 메서드로 접근 가능할 것이라는 순진한 생각을 가졌습니다. 저는 자바스크립트 생태계에 대한 높은 신뢰를 갖는 실수를 오늘도 하고 말았습니다.

부조리 맞습니다. 나중에 이거 수정하는 PR올려 컨트리뷰터가 되고 싶네요.