feat(screenshot): Improve Firebase Functions for screenshot tests#3732
Conversation
tinayuangao
commented
Mar 23, 2017
- Based on feat(screenshot): Use firebase functions and jwt to make screenshot tests more secure #3628
- Fixes Screenshot Testing - Firebase Functions #3686
- Use typescript. Put functions in seperate files.
- Add function to update Github Status based on result
- Add function to approve a PR (copy test images to goldens)
- Remove functions to upload filenames (We don't need it anymore)
8bcbdd1 to
675197f
Compare
|
@tinayuangao Should I review this once #3628 is merged? |
675197f to
16ee200
Compare
|
@devversion Yes. I will remove |
1f67986 to
dd86b5d
Compare
|
|
||
| /** | ||
| * Convert data to images. Image data posted to database will be saved as png files | ||
| * and upload to screenshot/$prNumber/dataType/$filename |
| let data = event.data.val(); | ||
| let saveFilename = `${event.params.filename}.screenshot.png`; | ||
|
|
||
| if (dataType != 'diff' && dataType != 'test') { |
There was a problem hiding this comment.
Nit: It is always desired to use strict comparison - dataType !== 'diff'
| let result = event.data.val() == true; | ||
| let prNumber = event.params.prNumber; | ||
| return event.data.ref.parent.child('sha').once('value').then((sha: firebaseAdmin.database.DataSnapshot) => { | ||
| return setGithubStatus(sha.val(), |
There was a problem hiding this comment.
I think the API we used in /tools/gulp/util/github is a bit more convenient. You just pass in a JSON config instead of X parameters.
| "jsonwebtoken": "^7.3.0" | ||
| "jsonwebtoken": "^7.3.0", | ||
| "request": "^2.81.0", | ||
| "typescript": "^2.2.1" |
There was a problem hiding this comment.
I thought we build the functions/*.ts file before deploying and Firebase shouldn't care about it? If so then we can just remove it from the package file here.
| let keys: string[] = []; | ||
| let counter = 0; | ||
| snapshot.forEach((childSnapshot: firebaseAdmin.database.DataSnapshot) => { | ||
| if (childSnapshot.val() == false) { |
| keys.push(childSnapshot.key); | ||
| } | ||
| counter ++; | ||
| if (counter == snapshot.numChildren()) return true; |
There was a problem hiding this comment.
Why do we need to manually cancel the enumeration? Doesn't the loop stop automatically when you iterated through all?
There was a problem hiding this comment.
It is.
But typescript requires return boolean, and once boolean returned the forEach loop will end.
| export function copyTestImagesToGoldens(prNumber: string) { | ||
| return firebaseAdmin.database().ref(`screenshot/reports/${prNumber}/results`).once('value') | ||
| .then((snapshot: firebaseAdmin.database.DataSnapshot) => { | ||
| let keys: string[] = []; |
| "jwt_util.ts", | ||
| "test_goldens.ts", | ||
| "util/github.ts", | ||
| "util/jwt.ts" |
There was a problem hiding this comment.
Assuming that index.ts is your entry-point and all other files are somehow imported by it. You could just add index.ts as file her.e
| description: string, | ||
| url: string, | ||
| repoSlug: string, | ||
| token: string) { |
There was a problem hiding this comment.
As mentioned above, I would prefer the syntax / API we currently have on tools/gulp/util/github.ts
| function updateFileResult(database: firebase.database.Database, prNumber: string, | ||
| filenameKey: string, result: boolean) { | ||
| return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result); | ||
| return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result) ; |
There was a problem hiding this comment.
Unused extra space at line end?
|
Comments addressed. Please take another look. Thanks! |
| * temporary folder if the token is valid. | ||
| * Move the data to 'screenshot/reports/$prNumber/$path | ||
| */ | ||
| export function verifyJWTAndUpdateData(event: any, path: string) { |
There was a problem hiding this comment.
verifyJwtAndTransferResultToTrustedLocation?
| @@ -0,0 +1,22 @@ | |||
| import * as firebaseAdmin from 'firebase-admin'; | |||
There was a problem hiding this comment.
Could there be a more specific name for this file than data.ts? Maybe verify-and-copy-screenshot-report.ts or copy-trusted-screenshot-report.ts?
In general the screenshot-related files under functions/ should have screenshot in the name somewhere (since we're likely going to add more firebase functions). A screenshot-test/ subdirectory would also work if firebase supports that.
| * Handle data written to temporary folder. Validate the JWT and move the data out of | ||
| * temporary folder if the token is valid. | ||
| * Move the data to 'screenshot/reports/$prNumber/$path | ||
| */ |
There was a problem hiding this comment.
/**
* Verifies that a screenshot report is valid (trusted via JWT) and, if so, copies it from the
* temporary, unauthenticated location to the more permanent, trusted location.
*/| */ | ||
| export function verifyJWTAndUpdateData(event: any, path: string) { | ||
| // Only edit data when it is first created. Exit when the data is deleted. | ||
| if (event.data.previous.exists() || !event.data.exists()) { |
There was a problem hiding this comment.
Would it make sense to create utility functions for things like this? E.g.,
isEditEvent(event) {
return event.data.previous.exists() && event.data.exists();
}
isDeleteEvent(event) {
return event.data.previous.exists() && !event.data.exists();
}
isCreateEvent(event) {
return !event.data.previous.exists() && event.data.exists();
}| * Verify event params have correct JsonWebToken, and execute callback when the JWT is verified. | ||
| * Delete the data if there's an error or the callback is done | ||
| */ | ||
| export function verifySecureTokenAndExecute(event: firebaseFunctions.Event<any>) { |
There was a problem hiding this comment.
Since this returns a promise, I would just call the function verifySecureToken. That way, then calling it, it will read like
"Verify secure token, then ..."
| return firebaseAdmin.database().ref().child('screenshot/reports') | ||
| .child(prNumber).child(path).set(data); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
How much of the overall screenshot code could be moved to tools/screenshot-testing and how much needs to live in functions/? I'd like to try to package everything under tools/ as the shareable package that can be used in other repos. Ideally the bits that live in functions/ would be very minimal.
| * Convert data to images. Image data posted to database will be saved as png files | ||
| * and uploaded to screenshot/$prNumber/dataType/$filename | ||
| */ | ||
| export function convertTestImageDataToFiles(event: any) { |
There was a problem hiding this comment.
"Convert data to images"
What format is the data that's being converted?
| let data = event.data.val(); | ||
| let saveFilename = `${event.params.filename}.screenshot.png`; | ||
|
|
||
| if (dataType !== 'diff' && dataType !== 'test') { |
There was a problem hiding this comment.
Add a comment explaining what this condition is checking for?
| const jwt = require('jsonwebtoken'); | ||
| const fs = require('fs'); | ||
|
|
||
| Object.defineProperty(exports, "__esModule", { value: true }); |
There was a problem hiding this comment.
What's going on with these imports? At the very least it needs an explanatory comment.
There was a problem hiding this comment.
I think that file was accidentally committed? There is also the index.ts. @tinayuangao
| var image_data_1 = require("./image_data"); | ||
| var data_image_1 = require("./data_image"); | ||
| var test_goldens_1 = require("./test_goldens"); | ||
| var github_1 = require("./github"); |
There was a problem hiding this comment.
Prefer single-quotes over double-quotes.
| # dependencies | ||
| /node_modules | ||
| /functions/node_modules | ||
| /tools/screenshot-test/functions/node_modules |
There was a problem hiding this comment.
If you remove the slash for the /node_modules above. Then it will include all node_modules/ folders in the whole project.
Just do
node_modules/
fffb5f0 to
62b56a3
Compare
| if (prNumber) { | ||
|
|
||
| if (isTravisPushBuild()) { | ||
| // Only update goldens for build |
There was a problem hiding this comment.
// Only update goldens for commits to master
?
There was a problem hiding this comment.
Also, it might make sense to rename isTravisPushBuild to isTravisMasterBuild (or invert the function and rename it to isTravisPullRequestBuild)
| const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); | ||
|
|
||
| /** | ||
| * Convert data to images. Image data posted to database will be saved as png files |
There was a problem hiding this comment.
Is the incoming data a base-64 encoded image? If so, the comment should say something like
"Writes base-64 encoded test images to png files on the filesystem."
I'd also rename the function to writeTestImagesToFiles
| @@ -0,0 +1,39 @@ | |||
| import * as firebaseFunctions from 'firebase-functions'; | |||
There was a problem hiding this comment.
Can you use dashes in the filenames instead of underscores?
I would also call this file write-images.ts
| /** | ||
| * Read golden files under /goldens/ and store the image data to | ||
| * database /screenshot/goldens/$filename | ||
| * Convert png image files to BufferArray data |
There was a problem hiding this comment.
Is this a base-64 encoded string?
| * database /screenshot/goldens/$filename | ||
| * Convert png image files to BufferArray data | ||
| */ | ||
| export function convertGoldenImagesToData(name: string, resourceState: string, fileBucket: any) { |
There was a problem hiding this comment.
Maybe copyGoldImagesToDatabase?
| } | ||
| counter++; | ||
| if (counter == snapshot.numChildren()) { | ||
| return true; |
There was a problem hiding this comment.
I don't think this true is used
There was a problem hiding this comment.
Here the type check requires return a boolean.
| }); | ||
| return failedFilenames; | ||
| }).then((failedFilenames: string[]) => { | ||
| return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { |
3d4887a to
f1edc9e
Compare
8c970fc to
abe107d
Compare
abe107d to
c2e8cbc
Compare
|
LGTM |
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |