Firebase functions 6 spotify 검색 api

남궁현·2022년 1월 31일
0

spotify 검색 api를 이용하여 어드민 페이지에서 음악을 검색할때 사용할 api를 하나 만들것이다. 이런식으로 자동완성에 사용될 것이다.

코드 작성

기존코드에서 getSpotifyFirebaseCustomToken 함수의 반환값에 uid를 추가해주었고 accessToken, refreshToken을 firebase auth에 저장함.

// src/index.ts
import {https} from "firebase-functions";
import Spotify from "spotify-web-api-node";
import _admin from "firebase-admin";
import {UserRecord} from "firebase-functions/v1/auth";
import {HttpsError} from "firebase-functions/v1/https";

export const admin =
  process.env.NODE_ENV === "test" // 테스트 환경이라면
    ? _admin.initializeApp({
        // 로컬에서 인증서를 가져옴
        credential: _admin.credential.cert(
          // eslint-disable-next-line @typescript-eslint/no-var-requires
          require("../testServiceAccountKey.json"),
        ),
      })
    : // 테스트 환경이 아니라면 firebase의 명령어로 실행하기에 자동으로 초기화됨
      _admin.initializeApp();

const spotify = new Spotify({
  clientId: "9ed1177dd6a4429db6fd2a025cb8ffb1",
  clientSecret: "97081903623b4ffb8654043f8c8d553e",
  redirectUri: "http://localhost/callback",
});

export const getSpotifyOAuthUrl = https.onCall(async () => {
  const url = spotify.createAuthorizeURL(["user-read-email"], "");
  return url;
});

export const getSpotifyFirebaseCustomToken = https.onCall(
  async (data: {spotifyCode: string}) => {
    const {spotifyCode} = data;

    // 로그인 토큰을 받아와서 accessToken과 refreshToken으로 변환함
    const credential = await spotify.authorizationCodeGrant(spotifyCode);
    // 유저 세부 정보를 가져옴
    spotify.setAccessToken(credential.body.access_token); // getMe() accessToken을 기준으로 정보를 가져오기 때문에 적용
    const me = await spotify.getMe();

    const uid = me.body.id; // firebase auth에서 id로 사용할 값
    const email = me.body.email; // firebase auth에 저장할 이메일

    let user: UserRecord;

    try {
      // 이미 유저가 있다면 내용을 업데이트함
      user = await admin.auth().updateUser(uid, {email});
    } catch (_error) {
      // 유저가 없다는 내용의 에러인지 확인
      const error = _error as any;
      if (error.errorInfo.code !== "auth/user-not-found") throw error;
      // 없다면 유저를 생성함
      user = await admin.auth().createUser({uid, email});
    }

    // User 데이터에 spotify refreshToken, accessToken 저장해둠
    await admin.auth().setCustomUserClaims(uid, {
      refreshToken: credential.body.refresh_token,
      accessToken: credential.body.access_token,
    });
    // 앱에서 firebase auth를 사용하여 로그인을 관리할 수 있도록 firebase token을 생성
    const token = await admin.auth().createCustomToken(user.uid);
    return {token, uid};
  },
);

export const searchTracks = https.onCall(
  async (data: {query: string}, context) => {
    const {query} = data;
    // 비로그인 차단
    if (!context.auth)
      throw new HttpsError("unauthenticated", "로그인을 해주세요");

    // 유저정보 조회
    const user = await admin.auth().getUser(context.auth.uid);
    const accessToken = user.customClaims?.accessToken;
    const refreshToken = user.customClaims?.refreshToken;

    // 기본 적으로 accessToken을 기준으로 API를 사용함
    spotify.setAccessToken(accessToken);

    let result: any;

    try {
      // 요청
      result = await spotify.searchTracks(query, {limit: 20});
    } catch (error) {
      // accessToken 이 만료되어 오류가 발생했다면
      // accessToken이 만료되어 refreshToken을 사용하여 제발급 받음
      spotify.setRefreshToken(refreshToken);
      const {body} = await spotify.refreshAccessToken(); // 제발급 API
      // firebase auth에 변경된 accessToken 저장
      await admin
        .auth()
        .setCustomUserClaims(user.uid, {accessToken: body.access_token});
      // 다시 accessToken 지정
      spotify.setAccessToken(body.access_token);
      // 다시 호출
      result = await spotify.searchTracks(query, {limit: 20});
    }

    return result.body.tracks;
  },
);

테스트 코드 작성

  • 트랙 리스트를 반환하는지
  • 비로그인시 오류를 반환하는지
    기존의 코드에서 getSpotifyFirebaseCustomToken에 반환값에 대한 테스트 코드를 변경해주었고 이과정에서 uid를 캐싱함
import {expect} from "chai";
import firebaseFunctionsTest from "firebase-functions-test";
import puppeteer from "puppeteer";

const testFunctions = firebaseFunctionsTest();

describe("/", () => {
  let Functions: any;
  let loginUrl: string;
  let token: string;
  let uid: string;

  before(() => {
    Functions = require("../src/index.ts");
  });

  context("getSpotifyOAuthUrl", () => {
    it("스포티파이로 로그인 할 수 있는 주소를 반환합니다.", async () => {
      const result = await testFunctions.wrap(Functions.getSpotifyOAuthUrl)({});
      loginUrl = result; // 웹뷰에서 띄우기 위해 변수로 저장해둠
      expect(result).to.be.a("string"); // 문자열로 반환하는지 테스트
      expect(result).to.include("https://accounts.spotify.com/authorize"); // 유효한 주소인지 확인
    });
  });

  context("실제 로그인 테스트", () => {
    it("웹뷰로 로그인 중...", async () => {
      // 브라우저 실행
      const browser = await puppeteer.launch({
        headless: true, // 백그라운드에서 웹뷰를 띄울지, 실제 배포시에는 true로 변경
        timeout: 15000,
      });
      const page = await browser.newPage(); // 페이지 하나 생성
      await page.goto(loginUrl); // getSpotifyOAuthUrl에서 받은 url로 이동
      await page.type("#login-username", "musicshortsvelog@gmail.com"); // 아이디 input에 테스트계정 아이디 삽입
      await page.type("#login-password", "musicshorts12!@"); // 페스워드 input에 비밀번호 삽입
      await page.waitForTimeout(1000); // 삽입된데이터 적용시간
      await page.click("#login-button"); // 로그인 작동
      await new Promise<void>(res =>
        page.on("response", event => {
          // 302 리다이렉트 요청이 왓을때
          if (event.status() === 302) {
            // 리다이렉트할 주소의 url query에 "code"를 가져와 token 저장
            token = event.headers().location.split("code=")[1].split("&")[0];
            res(); // 프로미스 탈출
          }
        }),
      );
      await browser.close(); // 브라우저 닫기
      expect(token.length).to.be.greaterThanOrEqual(1); // 유효한 토큰인지 테스트
    }).timeout(15000);
  });

  context("getSpotifyFirebaseCustomToken", () => {
    it("함수 호출시 파이어베이스 토큰과 uid를 반환합니다.", async () => {
      const result = await testFunctions.wrap(
        Functions.getSpotifyFirebaseCustomToken,
      )({spotifyCode: token});
      uid = result.uid; // uid 캐싱
      expect(result.token).to.be.a("string"); // 토큰을 반환하는지
      expect(result.uid).to.be.a("string"); // uid를 반환하는지
    }).timeout(5000);
  });

  context("searchTracks", () => {
    it("함수 호출시 트랙 리스트를 반환합니다.", async () => {
      const result = await testFunctions.wrap(Functions.searchTracks)(
        {query: "just the two of us"},
        {auth: {uid}}, // 위에서 생성한 테스트 계정으로 로그인
      );
      expect(result.items).to.be.a("array");
    }).timeout(5000);

    it("로그인 필수 입니다.", async () => {
      const result = await testFunctions
        .wrap(Functions.searchTracks)({query: "just the two of us"})
        .catch((e: any) => e);
      expect(result).to.be.a("error");
    });
  });
});

테스트 실행

0개의 댓글