Firebase functions 6 spotify 검색 api

남궁현·2022년 1월 31일

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개의 댓글