11'use strict' ;
22
3- const functions = require ( 'firebase-functions' ) ;
3+ const firebaseFunctions = require ( 'firebase-functions' ) ;
4+ const firebaseAdmin = require ( 'firebase-admin' ) ;
45const gcs = require ( '@google-cloud/storage' ) ( ) ;
5- const admin = require ( 'firebase-admin' ) ;
66const jwt = require ( 'jsonwebtoken' ) ;
77const 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 */
1134const 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 ;
2055 if ( dataTypes . indexOf ( dataType ) == - 1 ) {
2156 return ;
@@ -24,71 +59,69 @@ exports.copyData = functions.database.ref('/temp/screenshot/reports/{prNumber}/{
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
94127function 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+
114157function 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}
0 commit comments