diff --git a/.travis.yml b/.travis.yml index 5d3e851f7b2a..7930c6831920 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,12 @@ sudo: false node_js: - '6.9.4' +addons: + jwt: + # SAUCE_ACCESS_KEY<=secret for FIREBASE_ACCESS_TOKEN to work around travis-ci/travis-ci#7223, unencrypted value in valentine as FIREBASE_ACCESS_TOKEN> + # we alias FIREBASE_ACCESS_TOKEN to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there + - secure: "PKts/IbxuJRWWOEeiGbl8Z9zds0M+hIdCH/g/E4WbQ9yzSvSbdwzfmRfFccQFjxjsrY7+SJMVjsURZy+xUyBpzqgWYHUItnSVqjZb8DlyAU2IXyg8TM9BVLkGGe6k5k4PIFVmfMMMzQwWMM0X0W9w3oYmfHL5egxwSHvf9HIqLolLNXg8sqamIdS5d5KoCXf1c+oRjN/IMBktzNBR6N4OFOZQXVoepXNiIvTWAcTtOPBvFWdKP2n7RVioHKdm4a85aCUpDJp+LYGaLqiQZoRzmzfVTnAhTAPdd4ao5w/+jojrfZIHV55bqYF9rLnQMTneKsiyVNVYJzOLuxmARa/EEKfZld+J3rX4/o4cogrU38YSZF+T7J9g/7CTsnIZ3F6W6m+8iJbIBh55nGOQi5PVe458Q/nGb3fgQd2Z4+6lK9k479H4Ssh/Y7hbVQbepqEVIXzZKqWX6/ZE4iWoR/Q2dm0hySFmmB/R2etixX5JxhnHvgobTYIQ+1liJVp/3YFW1ru64Yg6yz/V291Bhh9g31znmTROCJ/usAmZZaLUqW1TDKnLIMP+M74MF9XERqcWKywXRFwxP4E5uDnx/vAyN49gL+SDfrBUxUtXrTkKZAlglwo9SgA7cOYEPWrionvKcGm87gCBYHFUmXZNQVzh212fpuJYXb/vy0sPDj8La4=" + branches: only: - master @@ -12,11 +18,11 @@ env: global: - LOGS_DIR=/tmp/angular-material2-build/logs - SAUCE_USERNAME=angular-ci - - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 - BROWSER_STACK_USERNAME=angularteam1 - BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB - BROWSER_PROVIDER_READY_FILE=/tmp/angular-material2-build/readyfile - BROWSER_PROVIDER_ERROR_FILE=/tmp/angular-material2-build/errorfile + matrix: # Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete. - MODE=lint @@ -32,6 +38,9 @@ matrix: - env: "MODE=saucelabs_optional" - env: "MODE=browserstack_optional" +before_install: + - source ./scripts/ci/env.sh + install: - npm install diff --git a/functions/config.json b/functions/config.json new file mode 100644 index 000000000000..4ac181e4769b --- /dev/null +++ b/functions/config.json @@ -0,0 +1,9 @@ +{ + "firebase": { + "apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg", + "authDomain": "material2-screenshots.firebaseapp.com", + "databaseURL": "https://material2-screenshots.firebaseio.com", + "storageBucket": "material2-screenshots.appspot.com", + "messagingSenderId": "975527407245" + } +} diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 000000000000..ec7a0f3ac675 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,190 @@ +'use strict'; + +const firebaseFunctions = require('firebase-functions'); +const firebaseAdmin = require('firebase-admin'); +const gcs = require('@google-cloud/storage')(); +const jwt = require('jsonwebtoken'); +const fs = require('fs'); + +/** + * Data and images handling for Screenshot test. + * + * All users can post data to temporary folder. These Functions will check the data with JsonWebToken and + * move the valid data out of temporary folder. + * + * For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to + * /screenshot/reports/$prNumber. + * These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information + * + * For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, save the image + * data to image files and upload to google cloud storage under location /screenshots/$prNumber + * These are screenshot test result images, and difference images generated from screenshot comparison. + * + * For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database + * under location /screenshot/goldens + * Screenshot tests can only read restricted database data with no credentials, and they cannot access + * Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests. + * + * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage. + * All invalid data will be removed. + * The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path. + */ + +// Initailize the admin app +firebaseAdmin.initializeApp(firebaseFunctions.config().firebase); + +/** The valid data types database accepts */ +const dataTypes = ['filenames', 'commit', 'result', 'sha', 'travis']; + +/** The repo slug. This is used to validate the JWT is sent from correct repo. */ +const repoSlug = firebaseFunctions.config().repo.slug; + +/** The JWT secret. This is used to validate JWT. */ +const secret = firebaseFunctions.config().secret.key; + +/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */ +const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); + +/** The Json Web Token format. The token is stored in data path. */ +const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}'; + +/** The temporary folder name for screenshot data that needs to be validated via JWT. */ +const tempFolder = '/untrustedInbox'; + +/** + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber + * Data copied: filenames(image results names), commit(github PR info), + * sha (github PR info), result (true or false for all the tests), travis job number + */ +const copyDataPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/{dataType}`; +exports.copyData = firebaseFunctions.database.ref(copyDataPath).onWrite(event => { + const dataType = event.params.dataType; + if (dataTypes.includes(dataType)) { + return verifyAndCopyScreenshotResult(event, dataType); + } +}); + +/** + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber + * Data copied: test result for each file/test with ${filename}. The value should be true or false. + */ +const copyDataResultPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/results/{filename}`; +exports.copyDataResult = firebaseFunctions.database.ref(copyDataResultPath).onWrite(event => { + return verifyAndCopyScreenshotResult(event, `results/${event.params.filename}`); +}); + +/** + * Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ to storage /screenshots/$prNumber + * Data copied: test result images. Convert from data to image files in storage. + */ +const copyImagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/{dataType}/{filename}`; +exports.copyImage = firebaseFunctions.database.ref(copyImagePath).onWrite(event => { + // Only edit data when it is first created. Exit when the data is deleted. + if (event.data.previous.exists() || !event.data.exists()) { + return; + } + + const dataType = event.params.dataType; + const prNumber = event.params.prNumber; + const secureToken = getSecureToken(event); + const saveFilename = `${event.params.filename}.screenshot.png`; + + if (dataType != 'diff' && dataType != 'test') { + return; + } + + return verifySecureToken(secureToken, prNumber).then((payload) => { + const tempPath = `/tmp/${dataType}-${saveFilename}` + const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`; + const binaryData = new Buffer(event.data.val(), 'base64').toString('binary'); + fs.writeFile(tempPath, binaryData, 'binary'); + return bucket.upload(tempPath, {destination: filePath}).then(() => { + // Clear the data in temporary folder after processed. + return event.data.ref.parent.set(null); + }); + }).catch((error) => { + console.error(`Invalid secure token ${secureToken} ${error}`); + return event.data.ref.parent.set(null); + }); +}); + +/** + * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ + * so we can read the goldens without credentials. + */ +exports.copyGoldens = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket) + .object().onChange(event => { + // The filePath should always l ook like "goldens/xxx.png" + const filePath = event.data.name; + + // Get the file name. + const fileNames = filePath.split('/'); + if (fileNames.length != 2 && fileNames[0] != 'goldens') { + return; + } + const filenameKey = fileNames[1].replace('.screenshot.png', ''); + + // When a gold image is deleted, also delete the corresponding record in the firebase database. + if (event.data.resourceState === 'not_exists') { + return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(null); + } + + // Download file from bucket. + const bucket = gcs.bucket(event.data.bucket); + const tempFilePath = `/tmp/${fileNames[1]}`; + return bucket.file(filePath).download({destination: tempFilePath}).then(() => { + const data = fs.readFileSync(tempFilePath); + return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(data); + }); +}); + +/** + * Handle data written to temporary folder. Validate the JWT and move the data out of + * temporary folder if the token is valid. + */ +function verifyAndCopyScreenshotResult(event, path) { + // Only edit data when it is first created. Exit when the data is deleted. + if (event.data.previous.exists() || !event.data.exists()) { + return; + } + + const prNumber = event.params.prNumber; + const secureToken = getSecureToken(event); + const original = event.data.val(); + + return verifySecureToken(secureToken, prNumber).then((payload) => { + return firebaseAdmin.database().ref().child('screenshot/reports') + .child(prNumber).child(path).set(original).then(() => { + // Clear the data in temporary folder after processed. + return event.data.ref.parent.set(null); + }); + }).catch((error) => { + console.error(`Invalid secure token ${secureToken} ${error}`); + return event.data.ref.parent.set(null); + }); +} + +/** + * Extract the Json Web Token from event params. + * In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}. + * Replace '/' with '.' to get the token. + */ +function getSecureToken(event) { + return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`; +} + +function verifySecureToken(token, prNumber) { + return new Promise((resolve, reject) => { + jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => { + if (err) { + reject(err.message || err); + } else if (payload.slug !== repoSlug) { + reject(`jwt slug invalid. expected: ${repoSlug}`); + } else if (payload['pull-request'].toString() !== prNumber) { + reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`); + } else { + resolve(payload); + } + }); + }); +} diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 000000000000..180b444d89e8 --- /dev/null +++ b/functions/package.json @@ -0,0 +1,10 @@ +{ + "name": "angular-material2-functions", + "description": "Angular Material2 screenshot test functions", + "dependencies": { + "@google-cloud/storage": "^0.8.0", + "firebase-admin": "^4.1.3", + "firebase-functions": "^0.5.2", + "jsonwebtoken": "^7.3.0" + } +} diff --git a/package.json b/package.json index 9f5ba36c75ff..5689f73c5544 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "dgeni-packages": "^0.16.5", "firebase-admin": "^4.1.2", "firebase-tools": "^2.2.1", + "firebase": "^3.7.2", "fs-extra": "^2.0.0", "glob": "^7.1.1", "google-cloud": "^0.48.0", diff --git a/scripts/ci/env.sh b/scripts/ci/env.sh new file mode 100644 index 000000000000..d981c4625818 --- /dev/null +++ b/scripts/ci/env.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + + +if [[ ${TRAVIS:-} ]]; then + # If FIREBASE_ACCESS_TOKEN not set yet, export the FIREBASE_ACCESS_TOKEN using the JWT token that Travis generated and exported for SAUCE_ACCESS_KEY. + # This is a workaround for travis-ci/travis-ci#7223 + # WARNING: FIREBASE_ACCESS_TOKEN should NOT be printed + export FIREBASE_ACCESS_TOKEN=${FIREBASE_ACCESS_TOKEN:-$SAUCE_ACCESS_KEY} + + # - we overwrite the value set by Travis JWT addon here to work around travis-ci/travis-ci#7223 for FIREBASE_ACCESS_TOKEN + export SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 +fi diff --git a/tools/gulp/tasks/e2e.ts b/tools/gulp/tasks/e2e.ts index a60b43b9a69d..f84859244dd2 100644 --- a/tools/gulp/tasks/e2e.ts +++ b/tools/gulp/tasks/e2e.ts @@ -57,4 +57,6 @@ task('e2e', sequenceTask( [':test:protractor:setup', 'serve:e2eapp'], ':test:protractor', ':serve:e2eapp:stop', + 'screenshots', )); + diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 8ad9b9fbd91a..7859929ddf7e 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -1,55 +1,68 @@ import {task} from 'gulp'; -import {readdirSync, statSync, existsSync, mkdirp} from 'fs-extra'; +import {readdirSync, statSync, existsSync, mkdirp, readFileSync, writeFileSync} from 'fs-extra'; import * as path from 'path'; import * as admin from 'firebase-admin'; -import {openScreenshotsBucket, openFirebaseScreenshotsDatabase} from '../util/firebase'; +import * as firebase from 'firebase'; +import { + openScreenshotsBucket, + openFirebaseScreenshotsDatabase, + connectFirebaseScreenshots} from '../util/firebase'; import {setGithubStatus} from '../util/github'; const imageDiff = require('image-diff'); +// Directory to which untrusted screenshot results are temporarily written +// (without authentication required) before they are verified and copied to +// the final storage location. +const TEMP_FOLDER = 'untrustedInbox'; const SCREENSHOT_DIR = './screenshots'; -const FIREBASE_REPORT = 'screenshot/reports'; +const FIREBASE_REPORT = `${TEMP_FOLDER}/screenshot/reports`; +const FIREBASE_IMAGE = `${TEMP_FOLDER}/screenshot/images`; const FIREBASE_FILELIST = 'screenshot/filenames'; /** Task which upload screenshots generated from e2e test. */ task('screenshots', () => { let prNumber = process.env['TRAVIS_PULL_REQUEST']; if (prNumber) { - let database = openFirebaseScreenshotsDatabase(); + let firebaseApp = connectFirebaseScreenshots(); + let database = firebaseApp.database(); + return getScreenshotFiles(database) - .then((files: any[]) => downloadAllGoldsAndCompare(files, database, prNumber)) + .then(() => downloadAllGoldsAndCompare(database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) .then((result: boolean) => updateGithubStatus(prNumber, result)) - .then(() => uploadScreenshots('diff', prNumber)) - .then(() => uploadScreenshots('test', prNumber)) + .then(() => uploadScreenshotsData(database, 'diff', prNumber)) + .then(() => uploadScreenshotsData(database, 'test', prNumber)) .then(() => updateTravis(database, prNumber)) .then(() => setScreenFilenames(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } else if (process.env['TRAVIS']) { // Only update golds and filenames for build let database = openFirebaseScreenshotsDatabase(); - uploadScreenshots('gold') + uploadScreenshots() .then(() => setScreenFilenames(database)) .then(() => database.goOffline(), () => database.goOffline()); } }); -function updateFileResult(database: admin.database.Database, prNumber: string, +function updateFileResult(database: firebase.database.Database, prNumber: string, filenameKey: string, result: boolean) { return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result); } -function updateResult(database: admin.database.Database, prNumber: string, result: boolean) { +function updateResult(database: firebase.database.Database, prNumber: string, result: boolean) { return getPullRequestRef(database, prNumber).child('result').set(result).then(() => result); } -function getPullRequestRef(database: admin.database.Database, prNumber: string) { - return database.ref(FIREBASE_REPORT).child(prNumber); +function getPullRequestRef(database: firebase.database.Database | admin.database.Database, + prNumber: string) { + let secureToken = getSecureToken(); + return database.ref(FIREBASE_REPORT).child(prNumber).child(secureToken); } -function updateTravis(database: admin.database.Database, +function updateTravis(database: firebase.database.Database, prNumber: string) { - return database.ref(FIREBASE_REPORT).child(prNumber).update({ + return getPullRequestRef(database, prNumber).update({ commit: process.env['TRAVIS_COMMIT'], sha: process.env['TRAVIS_PULL_REQUEST_SHA'], travis: process.env['TRAVIS_JOB_ID'], @@ -57,11 +70,23 @@ function updateTravis(database: admin.database.Database, } /** Get a list of filenames from firebase database. */ -function getScreenshotFiles(database: admin.database.Database) { - let bucket = openScreenshotsBucket(); - return bucket.getFiles({ prefix: 'golds/' }).then(function(data: any) { - return data[0].filter((file: any) => file.name.endsWith('.screenshot.png')); - }); +function getScreenshotFiles(database: firebase.database.Database) { + mkdirp(path.join(SCREENSHOT_DIR, `golds`)); + mkdirp(path.join(SCREENSHOT_DIR, `diff`)); + + return database.ref('screenshot/goldens').once('value') + .then((snapshot: firebase.database.DataSnapshot) => { + let counter = 0; + snapshot.forEach((childSnapshot: firebase.database.DataSnapshot) => { + let key = childSnapshot.key; + let binaryData = new Buffer(childSnapshot.val(), 'base64').toString('binary'); + writeFileSync(`${SCREENSHOT_DIR}/golds/${key}.screenshot.png`, binaryData, 'binary'); + counter++; + if (counter == snapshot.numChildren()) { + return true; + } + }); + }).catch((error: any) => console.log(error)); } function extractScreenshotName(fileName: string) { @@ -74,50 +99,53 @@ function getLocalScreenshotFiles(dir: string): string[] { .filter((fileName: string) => fileName.endsWith('.screenshot.png')); } +/** + * Get processed secure token. The jwt token has 3 parts: header, payload, signature and has format + * {jwtHeader}.{jwtPayload}.{jwtSignature} + * The three parts is connected by '.', while '.' is not a valid path in firebase database. + * Replace all '.' to '/' to make the path valid + * Output is {jwtHeader}/{jwtPayload}/{jwtSignature}. + * This secure token is used to validate the write access is from our TravisCI under our repo. + * All data is written to /$path/$secureToken/$data and after validated the + * secure token, the data is moved to /$path/$data in database. + */ +function getSecureToken() { + return process.env['FIREBASE_ACCESS_TOKEN'].replace(/[.]/g, '/'); +} + /** * Upload screenshots to google cloud storage. * @param prNumber - The key used in firebase. Here it is the PR number. - * If there's no prNumber, we will upload images to 'golds/' folder - * @param mode - Can be 'test' or 'diff' or 'gold'. + * @param mode - Can be 'test' or 'diff' . * If the images are the test results, mode should be 'test'. * If the images are the diff images generated, mode should be 'diff'. - * For golds mode should be 'gold'. */ -function uploadScreenshots(mode?: 'test' | 'diff' | 'gold', prNumber?: string) { - let bucket = openScreenshotsBucket(); - - let promises: any[] = []; +function uploadScreenshotsData(database: firebase.database.Database, + mode: 'test' | 'diff', prNumber: string) { let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; - getLocalScreenshotFiles(localDir).forEach((file: string) => { + let promises = getLocalScreenshotFiles(localDir).map((file: string) => { let fileName = path.join(localDir, file); - let destination = (mode == 'gold' || !prNumber) ? - `golds/${file}` : `screenshots/${prNumber}/${mode}/${file}`; - promises.push(bucket.upload(fileName, { destination: destination })); + let filenameKey = extractScreenshotName(fileName); + let secureToken = getSecureToken(); + let data = readFileSync(fileName); + return database.ref(FIREBASE_IMAGE).child(prNumber) + .child(secureToken).child(mode).child(filenameKey).set(data); }); return Promise.all(promises); } + /** Download golds screenshots. */ -function downloadAllGoldsAndCompare( - files: any[], database: admin.database.Database, - prNumber: string) { +function downloadAllGoldsAndCompare(database: firebase.database.Database, prNumber: string) { - mkdirp(path.join(SCREENSHOT_DIR, `golds`)); - mkdirp(path.join(SCREENSHOT_DIR, `diff`)); + let filenames = getLocalScreenshotFiles(path.join(SCREENSHOT_DIR, `golds`)); - return Promise.all(files.map((file: any) => { - return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber)); + return Promise.all(filenames.map((filename: string) => { + return diffScreenshot(filename, database, prNumber); })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } -/** Download one gold screenshot */ -function downloadGold(file: any) { - return file.download({ - destination: path.join(SCREENSHOT_DIR, file.name) - }); -} - -function diffScreenshot(filename: string, database: admin.database.Database, +function diffScreenshot(filename: string, database: firebase.database.Database, prNumber: string) { // TODO(tinayuangao): Run the downloads and diffs in parallel. filename = path.basename(filename); @@ -151,11 +179,11 @@ function diffScreenshot(filename: string, database: admin.database.Database, * Upload a list of filenames to firebase database as gold. * This is necessary for control panel since google-cloud is not available to client side. */ -function setScreenFilenames(database: admin.database.Database, +function setScreenFilenames(database: admin.database.Database | firebase.database.Database, prNumber?: string) { let filenames: string[] = getLocalScreenshotFiles(SCREENSHOT_DIR); let filelistDatabase = prNumber ? - database.ref(FIREBASE_REPORT).child(prNumber).child('filenames') : + getPullRequestRef(database, prNumber).child('filenames') : database.ref(FIREBASE_FILELIST); return filelistDatabase.set(filenames); } @@ -169,3 +197,14 @@ function updateGithubStatus(prNumber: number, result: boolean) { url: `http://material2-screenshots.firebaseapp.com/${prNumber}` }); } + +/** Upload screenshots to google cloud storage. */ +function uploadScreenshots() { + let bucket = openScreenshotsBucket(); + let promises = getLocalScreenshotFiles(SCREENSHOT_DIR).map((file: string) => { + let fileName = path.join(SCREENSHOT_DIR, file); + let destination = `golds/${file}`; + return bucket.upload(fileName, { destination: destination }); + }); + return Promise.all(promises); +} diff --git a/tools/gulp/util/firebase.ts b/tools/gulp/util/firebase.ts index d3ccaabf836f..213f284a6e5e 100644 --- a/tools/gulp/util/firebase.ts +++ b/tools/gulp/util/firebase.ts @@ -1,9 +1,12 @@ const firebaseAdmin = require('firebase-admin'); +const firebase = require('firebase'); const gcloud = require('google-cloud'); +const config = require('../../../functions/config.json'); + /** Opens a connection to the firebase realtime database. */ export function openFirebaseDashboardDatabase() { - // Initialize the Firebase application with admin credentials. + // Initialize the Firebase application with firebaseAdmin credentials. // Credentials need to be for a Service Account, which can be created in the Firebase console. firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert({ @@ -11,7 +14,7 @@ export function openFirebaseDashboardDatabase() { client_email: 'firebase-adminsdk-ch1ob@material2-dashboard.iam.gserviceaccount.com', // In Travis CI the private key will be incorrect because the line-breaks are escaped. // The line-breaks need to persist in the service account private key. - private_key: (process.env['MATERIAL2_FIREBASE_PRIVATE_KEY'] || '').replace(/\\n/g, '\n') + private_key: decode(process.env['MATERIAL2_FIREBASE_PRIVATE_KEY']) }), databaseURL: 'https://material2-dashboard.firebaseio.com' }); @@ -38,7 +41,7 @@ export function openScreenshotsBucket() { /** Opens a connection to the firebase database for screenshots. */ export function openFirebaseScreenshotsDatabase() { - // Initialize the Firebase application with admin credentials. + // Initialize the Firebase application with firebaseAdmin credentials. // Credentials need to be for a Service Account, which can be created in the Firebase console. let screenshotApp = firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert({ @@ -56,6 +59,14 @@ export function openFirebaseScreenshotsDatabase() { export function decode(str: string): string { // In Travis CI the private key will be incorrect because the line-breaks are escaped. // The line-breaks need to persist in the service account private key. - return (str || '').split('\\n').reverse().join('\\n').replace(/\\n/g, '\n'); + return (str || '').replace(/\\n/g, '\n'); +} + +/** + * Open firebase connection for screenshot + * This connection is client side connection with no credentials + */ +export function connectFirebaseScreenshots() { + return firebase.initializeApp(config.firebase); }