Skip to content

Commit fb89d44

Browse files
committed
address comments
1 parent 5085489 commit fb89d44

File tree

3 files changed

+134
-85
lines changed

3 files changed

+134
-85
lines changed

functions/index.js

Lines changed: 124 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,127 @@
11
'use strict';
22

3-
const functions = require('firebase-functions');
3+
const firebaseFunctions = require('firebase-functions');
4+
const firebaseAdmin = require('firebase-admin');
45
const gcs = require('@google-cloud/storage')();
5-
const admin = require('firebase-admin');
66
const jwt = require('jsonwebtoken');
77
const fs = require('fs');
88

9-
admin.initializeApp(functions.config().firebase);
9+
/**
10+
* Data and images handling for Screenshot test
11+
*
12+
* For valid data posted to database /temp/screenshot/reports/$prNumber/$secureToken, move it to
13+
* /screenshot/reports/$prNumber.
14+
* These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information
15+
*
16+
* For valid image datas written to database /temp/screenshot/images/$prNumber/$secureToken/, save the image
17+
* data to image files and upload to google cloud storage under location /screenshots/$prNumber
18+
* These are screenshot test result images, and difference images generated from screenshot comparison.
19+
*
20+
* For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database
21+
* under location /screenshot/goldens
22+
* Screenshot tests can only read restricted database data with no credentials, and they cannot access
23+
* Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests.
24+
*
25+
* The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage.
26+
* All invalid data will be removed.
27+
* The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path.
28+
*/
29+
30+
// Initailize the admin app
31+
firebaseAdmin.initializeApp(firebaseFunctions.config().firebase);
1032

33+
/** The valid data types database accepts */
1134
const dataTypes = ['filenames', 'commit', 'result', 'sha', 'travis'];
12-
const repoSlug = functions.config().repo.slug;
13-
const secret = functions.config().secret.key;
14-
const bucket = gcs.bucket(functions.config().firebase.storageBucket);
35+
36+
/** The repo slug. This is used to validate the JWT is sent from correct repo. */
37+
const repoSlug = firebaseFunctions.config().repo.slug;
38+
39+
/** The JWT secret. This is used to validate JWT. */
40+
const secret = firebaseFunctions.config().secret.key;
41+
42+
/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */
43+
const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket);
44+
45+
/** The Json Web Token format. The token is stored in data path. */
46+
const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}';
47+
48+
/** The temporary folder name */
49+
const tempFolder = '/temp';
1550

1651
/** Copy valid data from /temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber */
17-
exports.copyData = functions.database.ref('/temp/screenshot/reports/{prNumber}/{token1}/{token2}/{token3}/{dataType}')
18-
.onWrite(event => {
52+
const copyDataPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/{dataType}`;
53+
exports.copyData = firebaseFunctions.database.ref(copyDataPath).onWrite(event => {
1954
const dataType = event.params.dataType;
20-
if (dataTypes.indexOf(dataType) == -1) {
21-
return;
55+
if (dataTypes.includes(dataType)) {
56+
return handleDataChange(event, dataType);
2257
}
23-
return handleDataChange(event, dataType);
58+
return;
2459
});
2560

2661
/** Copy valid data from /temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber */
27-
exports.copyDataResult = functions.database.ref('/temp/screenshot/reports/{prNumber}/{token1}/{token2}/{token3}/results/{filename}')
28-
.onWrite(event => {
62+
const copyDataResultPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/results/{filename}`;
63+
exports.copyDataResult = firebaseFunctions.database.ref(copyDataResultPath).onWrite(event => {
2964
return handleDataChange(event, `results/${event.params.filename}`);
3065
});
3166

3267
/** Copy valid data from database /temp/screenshot/images/$prNumber/$secureToken/ to storage /screenshots/$prNumber */
33-
exports.copyImage = functions.database.ref('/temp/screenshot/images/{prNumber}/{token1}/{token2}/{token3}/{dataType}/{filename}')
34-
.onWrite(event => {
35-
// Only edit data when it is first created. Exit when the data is deleted.
36-
if (event.data.previous.exists() || !event.data.exists()) {
37-
return;
38-
}
39-
40-
const dataType = event.params.dataType;
41-
const prNumber = event.params.prNumber;
42-
const secureToken = `${event.params.token1}.${event.params.token2}.${event.params.token3}`;
43-
const saveFilename = `${event.params.filename}.screenshot.png`;
44-
45-
if (dataType != 'diff' && dataType != 'test') {
46-
return;
47-
}
48-
49-
return validateSecureToken(secureToken, prNumber).then((payload) => {
50-
const tempPath = `/tmp/${dataType}-${saveFilename}`
51-
const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`;
52-
const binaryData = new Buffer(event.data.val(), 'base64').toString('binary');
53-
fs.writeFile(tempPath, binaryData, 'binary');
54-
return bucket.upload(tempPath, {
55-
destination: filePath
56-
}).then(() => {
57-
return event.data.ref.parent.set(null);
58-
});
59-
}).catch((error) => {
60-
console.error(`Invalid secure token ${secureToken} ${error}`);
68+
const copyImagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/{dataType}/{filename}`;
69+
exports.copyImage = firebaseFunctions.database.ref(copyImagePath).onWrite(event => {
70+
// Only edit data when it is first created. Exit when the data is deleted.
71+
if (event.data.previous.exists() || !event.data.exists()) {
72+
return;
73+
}
74+
75+
const dataType = event.params.dataType;
76+
const prNumber = event.params.prNumber;
77+
const secureToken = getSecureToken(event);
78+
const saveFilename = `${event.params.filename}.screenshot.png`;
79+
80+
if (dataType != 'diff' && dataType != 'test') {
81+
return;
82+
}
83+
84+
return validateSecureToken(secureToken, prNumber).then((payload) => {
85+
const tempPath = `/tmp/${dataType}-${saveFilename}`
86+
const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`;
87+
const binaryData = new Buffer(event.data.val(), 'base64').toString('binary');
88+
fs.writeFile(tempPath, binaryData, 'binary');
89+
return bucket.upload(tempPath, {destination: filePath}).then(() => {
6190
return event.data.ref.parent.set(null);
6291
});
92+
}).catch((error) => {
93+
console.error(`Invalid secure token ${secureToken} ${error}`);
94+
return event.data.ref.parent.set(null);
95+
});
6396
});
6497

6598
/**
6699
* Copy valid goldens from storage /goldens/ to database /screenshot/goldens/
67-
* so we can read the goldens without credentials
100+
* so we can read the goldens without credentials.
68101
*/
69-
exports.copyGoldens = functions.storage.bucket(functions.config().firebase.storageBucket).object().onChange(event => {
70-
const filePath = event.data.name;
71-
72-
// Get the file name.
73-
const fileNames = filePath.split('/');
74-
if (fileNames.length != 2 && fileNames[0] != 'goldens') {
75-
return;
76-
}
77-
const filenameKey = fileNames[1].replace('.screenshot.png', '');
78-
79-
if (event.data.resourceState === 'not_exists') {
80-
return admin.database().ref(`screenshot/goldens/${filenameKey}`).set(null);
81-
}
82-
83-
// Download file from bucket.
84-
const bucket = gcs.bucket(event.data.bucket);
85-
const tempFilePath = `/tmp/${fileNames[1]}`;
86-
return bucket.file(filePath).download({
87-
destination: tempFilePath
88-
}).then(() => {
89-
const data = fs.readFileSync(tempFilePath);
90-
return admin.database().ref(`screenshot/goldens/${filenameKey}`).set(data);
91-
});
102+
exports.copyGoldens = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket)
103+
.object().onChange(event => {
104+
const filePath = event.data.name;
105+
106+
// Get the file name.
107+
const fileNames = filePath.split('/');
108+
if (fileNames.length != 2 && fileNames[0] != 'goldens') {
109+
return;
110+
}
111+
const filenameKey = fileNames[1].replace('.screenshot.png', '');
112+
113+
// When delete a file, remove the file in database
114+
if (event.data.resourceState === 'not_exists') {
115+
return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(null);
116+
}
117+
118+
// Download file from bucket.
119+
const bucket = gcs.bucket(event.data.bucket);
120+
const tempFilePath = `/tmp/${fileNames[1]}`;
121+
return bucket.file(filePath).download({destination: tempFilePath}).then(() => {
122+
const data = fs.readFileSync(tempFilePath);
123+
return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(data);
124+
});
92125
});
93126

94127
function handleDataChange(event, path) {
@@ -98,31 +131,41 @@ function handleDataChange(event, path) {
98131
}
99132

100133
const prNumber = event.params.prNumber;
101-
const secureToken = `${event.params.token1}.${event.params.token2}.${event.params.token3}`;
134+
const secureToken = getSecureToken(event);
102135
const original = event.data.val();
103136

104137
return validateSecureToken(secureToken, prNumber).then((payload) => {
105-
return admin.database().ref().child('screenshot/reports').child(prNumber).child(path).set(original).then(() => {
106-
return event.data.ref.parent.set(null);
107-
});
138+
return firebaseAdmin.database().ref().child('screenshot/reports')
139+
.child(prNumber).child(path).set(original).then(() => {
140+
return event.data.ref.parent.set(null);
141+
});
108142
}).catch((error) => {
109-
console.error(`Invalid secure token ${secureToken} ${error}`);
143+
console.error(`Invalid secure token ${secureToken} ${error}`);
110144
return event.data.ref.parent.set(null);
111145
});
112146
}
113147

148+
/**
149+
* Extract the Json Web Token from event params.
150+
* In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}.
151+
* Replace '/' with '.' to get the token.
152+
*/
153+
function getSecureToken(event) {
154+
return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`;
155+
}
156+
114157
function validateSecureToken(token, prNumber) {
115158
return new Promise((resolve, reject) => {
116-
jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
117-
if (err) {
118-
reject(err.message || err);
119-
} else if (payload.slug !== repoSlug) {
120-
reject(`jwt slug invalid. expected: ${repoSlug}`);
121-
} else if (payload['pull-request'].toString() !== prNumber) {
122-
reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`);
123-
} else {
124-
resolve(payload);
125-
}
126-
});
159+
jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
160+
if (err) {
161+
reject(err.message || err);
162+
} else if (payload.slug !== repoSlug) {
163+
reject(`jwt slug invalid. expected: ${repoSlug}`);
164+
} else if (payload['pull-request'].toString() !== prNumber) {
165+
reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`);
166+
} else {
167+
resolve(payload);
168+
}
169+
});
127170
});
128171
}

tools/gulp/tasks/screenshots.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,14 @@ function getLocalScreenshotFiles(dir: string): string[] {
9595
}
9696

9797
/**
98-
* Get processed secure token. The jwt token is connected by '.', while '.' is not a valid
99-
* path in firebase database. Replace all '.' to '/' to make the path valid
98+
* Get processed secure token. The jwt token has 3 parts: header, payload, signature and has format
99+
* {jwtHeader}.{jwtPayload}.{jwtSignature}
100+
* The three parts is connected by '.', while '.' is not a valid path in firebase database.
101+
* Replace all '.' to '/' to make the path valid
102+
* Output is {jwtHeader}/{jwtPayload}/{jwtSignature}.
103+
* This secure token is used to validate the write access is from our TravisCI under our repo.
104+
* All data is written to /$path/$secureToken/$data and after validated the
105+
* secure token, the data is moved to /$path/$data in database.
100106
*/
101107
function getSecureToken() {
102108
return process.env['FIREBASE_ACCESS_TOKEN'].replace(/[.]/g, '/');

tools/gulp/util/firebase.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const config = require('../../../functions/config.json');
66

77
/** Opens a connection to the firebase realtime database. */
88
export function openFirebaseDashboardDatabase() {
9-
// Initialize the Firebase application with admin credentials.
9+
// Initialize the Firebase application with firebaseAdmin credentials.
1010
// Credentials need to be for a Service Account, which can be created in the Firebase console.
1111
firebaseAdmin.initializeApp({
1212
credential: firebaseAdmin.credential.cert({
@@ -41,7 +41,7 @@ export function openScreenshotsBucket() {
4141

4242
/** Opens a connection to the firebase database for screenshots. */
4343
export function openFirebaseScreenshotsDatabase() {
44-
// Initialize the Firebase application with admin credentials.
44+
// Initialize the Firebase application with firebaseAdmin credentials.
4545
// Credentials need to be for a Service Account, which can be created in the Firebase console.
4646
let screenshotApp = firebaseAdmin.initializeApp({
4747
credential: firebaseAdmin.credential.cert({

0 commit comments

Comments
 (0)