- 
                Notifications
    You must be signed in to change notification settings 
- Fork 6.8k
feat(screenshot): Improve Firebase Functions for screenshot tests #3732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
         tinayuangao
  
      
      
      commented
      
            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
  
            
          
                functions/data_image.ts
              
                Outdated
          
        
      |  | ||
| /** | ||
| * Convert data to images. Image data posted to database will be saved as png files | ||
| * and upload to screenshot/$prNumber/dataType/$filename | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uploaded?
        
          
                functions/data_image.ts
              
                Outdated
          
        
      | let data = event.data.val(); | ||
| let saveFilename = `${event.params.filename}.screenshot.png`; | ||
|  | ||
| if (dataType != 'diff' && dataType != 'test') { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: It is always desired to use strict comparison - dataType !== 'diff'
        
          
                functions/github.ts
              
                Outdated
          
        
      | 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
        
          
                functions/package.json
              
                Outdated
          
        
      | "jsonwebtoken": "^7.3.0" | ||
| "jsonwebtoken": "^7.3.0", | ||
| "request": "^2.81.0", | ||
| "typescript": "^2.2.1" | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
        
          
                functions/test_goldens.ts
              
                Outdated
          
        
      | let keys: string[] = []; | ||
| let counter = 0; | ||
| snapshot.forEach((childSnapshot: firebaseAdmin.database.DataSnapshot) => { | ||
| if (childSnapshot.val() == false) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Strict comparsion
        
          
                functions/test_goldens.ts
              
                Outdated
          
        
      | keys.push(childSnapshot.key); | ||
| } | ||
| counter ++; | ||
| if (counter == snapshot.numChildren()) return true; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is.
But typescript requires return boolean, and once boolean returned the forEach loop will end.
        
          
                functions/test_goldens.ts
              
                Outdated
          
        
      | export function copyTestImagesToGoldens(prNumber: string) { | ||
| return firebaseAdmin.database().ref(`screenshot/reports/${prNumber}/results`).once('value') | ||
| .then((snapshot: firebaseAdmin.database.DataSnapshot) => { | ||
| let keys: string[] = []; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about failedFilenames?
        
          
                functions/tsconfig.json
              
                Outdated
          
        
      | "jwt_util.ts", | ||
| "test_goldens.ts", | ||
| "util/github.ts", | ||
| "util/jwt.ts" | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, I would prefer the syntax / API we currently have on tools/gulp/util/github.ts
        
          
                tools/gulp/tasks/screenshots.ts
              
                Outdated
          
        
      | 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused extra space at line end?
| Comments addressed. Please take another look. Thanks! | 
        
          
                functions/data.ts
              
                Outdated
          
        
      | * 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verifyJwtAndTransferResultToTrustedLocation?
        
          
                functions/data.ts
              
                Outdated
          
        
      | @@ -0,0 +1,22 @@ | |||
| import * as firebaseAdmin from 'firebase-admin'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
        
          
                functions/data.ts
              
                Outdated
          
        
      | * 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
  /**
   * 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.
   */        
          
                functions/data.ts
              
                Outdated
          
        
      | */ | ||
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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();
}        
          
                functions/jwt_util.ts
              
                Outdated
          
        
      | * 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 ..."
        
          
                functions/data.ts
              
                Outdated
          
        
      | return firebaseAdmin.database().ref().child('screenshot/reports') | ||
| .child(prNumber).child(path).set(data); | ||
| }); | ||
| }; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
        
          
                functions/data_image.ts
              
                Outdated
          
        
      | * 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment explaining what this condition is checking for?
        
          
                functions/index.js
              
                Outdated
          
        
      | const jwt = require('jsonwebtoken'); | ||
| const fs = require('fs'); | ||
|  | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's going on with these imports? At the very least it needs an explanatory comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that file was accidentally committed? There is also the index.ts. @tinayuangao
        
          
                functions/index.js
              
                Outdated
          
        
      | 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer single-quotes over double-quotes.
        
          
                .gitignore
              
                Outdated
          
        
      | # dependencies | ||
| /node_modules | ||
| /functions/node_modules | ||
| /tools/screenshot-test/functions/node_modules | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
  
            
          
                tools/gulp/tasks/screenshots.ts
              
                Outdated
          
        
      | if (prNumber) { | ||
|  | ||
| if (isTravisPushBuild()) { | ||
| // Only update goldens for build | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Only update goldens for commits to master
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe copyGoldImagesToDatabase?
| } | ||
| counter++; | ||
| if (counter == snapshot.numChildren()) { | ||
| return true; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this true is used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the type check requires return a boolean.
| }); | ||
| return failedFilenames; | ||
| }).then((failedFilenames: string[]) => { | ||
| return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arrow function here?
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. |