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");
});
});
});