1
1
'use strict' ;
2
2
3
- const functions = require ( 'firebase-functions' ) ;
3
+ const firebaseFunctions = require ( 'firebase-functions' ) ;
4
+ const firebaseAdmin = require ( 'firebase-admin' ) ;
4
5
const gcs = require ( '@google-cloud/storage' ) ( ) ;
5
- const admin = require ( 'firebase-admin' ) ;
6
6
const jwt = require ( 'jsonwebtoken' ) ;
7
7
const fs = require ( 'fs' ) ;
8
8
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 ) ;
10
32
33
+ /** The valid data types database accepts */
11
34
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' ;
15
50
16
51
/** 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 => {
19
54
const dataType = event . params . dataType ;
20
- if ( dataTypes . indexOf ( dataType ) == - 1 ) {
21
- return ;
55
+ if ( dataTypes . includes ( dataType ) ) {
56
+ return handleDataChange ( event , dataType ) ;
22
57
}
23
- return handleDataChange ( event , dataType ) ;
58
+ return ;
24
59
} ) ;
25
60
26
61
/** 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 => {
29
64
return handleDataChange ( event , `results/${ event . params . filename } ` ) ;
30
65
} ) ;
31
66
32
67
/** 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 ( ( ) => {
61
90
return event . data . ref . parent . set ( null ) ;
62
91
} ) ;
92
+ } ) . catch ( ( error ) => {
93
+ console . error ( `Invalid secure token ${ secureToken } ${ error } ` ) ;
94
+ return event . data . ref . parent . set ( null ) ;
95
+ } ) ;
63
96
} ) ;
64
97
65
98
/**
66
99
* 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.
68
101
*/
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
+ } ) ;
92
125
} ) ;
93
126
94
127
function handleDataChange ( event , path ) {
@@ -98,31 +131,41 @@ function handleDataChange(event, path) {
98
131
}
99
132
100
133
const prNumber = event . params . prNumber ;
101
- const secureToken = ` ${ event . params . token1 } . ${ event . params . token2 } . ${ event . params . token3 } ` ;
134
+ const secureToken = getSecureToken ( event ) ;
102
135
const original = event . data . val ( ) ;
103
136
104
137
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
+ } ) ;
108
142
} ) . catch ( ( error ) => {
109
- console . error ( `Invalid secure token ${ secureToken } ${ error } ` ) ;
143
+ console . error ( `Invalid secure token ${ secureToken } ${ error } ` ) ;
110
144
return event . data . ref . parent . set ( null ) ;
111
145
} ) ;
112
146
}
113
147
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
+
114
157
function validateSecureToken ( token , prNumber ) {
115
158
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
+ } ) ;
127
170
} ) ;
128
171
}
0 commit comments