Skip to content

Commit

Permalink
[test] Add framework for running Firestore emulator for unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
joshlf committed May 13, 2020
1 parent 1f26873 commit c0ba7cb
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 92 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ jobs:
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
- name: Install Google Cloud SDK components
run: yes | gcloud components install beta cloud-firestore-emulator
- name: Install Go
uses: actions/setup-go@v2
with:
Expand Down
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ reports.

1. Install [Go](https://golang.org/) 1.13 or higher
2. [Install the Google Cloud SDK](https://cloud.google.com/sdk/install).
3. Tests use the Firestore emulator, which requires some components from the
Google Cloud SDK which are not installed by default. In order to install
them, run the emulator once using `gcloud beta emulators firestore start`. It
will prompt to install any missing components. Once all the necessary
components have been installed and the emulator is actually running, you can
kill it.
3. Install components necessary to run the Firestore emulator: `gcloud
components install beta cloud-firestore-emulator`. Unit tests use the
emulator, so if these components are not installed, unit tests will fail.

## Run Tests

Expand Down Expand Up @@ -59,7 +56,8 @@ the new one and cause confusion).

If the `--host-port` flag is omitted, the emulator will choose a random port,
which has a high likelihood of not being in use. The emulator will output which
address to use by displaying a line like `export FIRESTORE_EMULATOR_HOST=::1:8195`.
address to use by displaying a line like `export
FIRESTORE_EMULATOR_HOST=::1:8195`.

Note that, if the local IP address is an IPv6 address (like `::1`), then you
will need to put square brackets around the address for compatibility with Go's
Expand All @@ -79,13 +77,13 @@ curl --request GET 'http://localhost:8080/challenge' \

### HTTPS

The local dev server uses HTTP so you must send a fake HTTPS header to prevent the
request from being rejected.
The local dev server uses HTTP so you must send a fake HTTPS header to prevent
the request from being rejected.

If you see an error message like this with HTTP Code 426:
If you see an error message like this with HTTP Code 418:

```
{"Error":"Please use HTTPS"}
{"message":"unsupported protocol HTTP; only HTTPS is supported"}
```

You should add either:
Expand All @@ -102,8 +100,9 @@ Forwarded: for=\"localhost\";proto=https

## Firebase Security

Unauthenticated Firestore access is disabled. If you want to bypass the firestore.rules
on the emulator REST interface just supply the auth bearer header value "owner":
Unauthenticated Firestore access is disabled. If you want to bypass the
firestore.rules on the emulator REST interface just supply the auth bearer
header value "owner":

```
Authorization: Bearer owner
Expand All @@ -119,5 +118,5 @@ $ ./deploy.sh

### Postman Collection

You can import the COVID-Watch.postman_collection.json to play with the local
You can import the `Covid Watch.postman_collection.json` to play with the local
Cloud Functions and Firestore Emulator.
3 changes: 2 additions & 1 deletion deploy.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#!/bin/sh
cd functions && gcloud functions deploy challenge --runtime go113 --trigger-http --entry-point ChallengeHandler --allow-unauthenticated
cd functions && gcloud functions deploy challenge --runtime go113 --trigger-http \
--entry-point ChallengeHTTPHandler --allow-unauthenticated
13 changes: 8 additions & 5 deletions functions/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import (
"upload-token.functions/internal/util"
)

// ChallengeHandler is a handler for the /challenge endpoint.
var ChallengeHandler = util.MakeHTTPHandler(challengeHandler)
// ChallengeHTTPHandler is an HTTP handler for the /challenge endpoint. It is
// intended to be registered as a Google Cloud Function by using the
// --entry-point flag to the `gcloud functions deploy` command.
var ChallengeHTTPHandler = util.MakeHTTPHandler(ChallengeHandler)

func challengeHandler(ctx *util.Context) util.StatusError {
if err := util.ValidateRequestMethod(ctx, "GET", ""); err != nil {
// ChallengeHandler is a handler for the /challenge endpoint.
func ChallengeHandler(ctx *util.Context) util.StatusError {
if err := ctx.ValidateRequestMethod("GET", ""); err != nil {
return err
}

Expand All @@ -22,4 +25,4 @@ func challengeHandler(ctx *util.Context) util.StatusError {
json.NewEncoder(ctx.HTTPResponseWriter()).Encode(c)

return nil
}
}
38 changes: 38 additions & 0 deletions functions/challenge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package functions

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"

"upload-token.functions/internal/pow"
"upload-token.functions/internal/util"
)

func TestChallenge(t *testing.T) {
firestore := util.NewTestFirestore(t)

req, err := http.NewRequestWithContext(context.Background(), "GET", "/challenge", nil)
assert.Nil(t, err)
r := httptest.NewRecorder()
ctx, err := util.NewTestContext(r, req, firestore)
assert.Nil(t, err)

ChallengeHandler(&ctx)

// First, unmarshal using pow.Challenge in order to benefit from its
// validation.
var c0 pow.Challenge
err = json.Unmarshal(r.Body.Bytes(), &c0)
assert.Nil(t, err)

// Second, unmarshal into a map so that we can inspect its contents.
var c1 map[string]interface{}
err = json.Unmarshal(r.Body.Bytes(), &c1)
assert.Nil(t, err)
assert.Equal(t, c1["work_factor"], float64(pow.DefaultWorkFactor))
}
7 changes: 4 additions & 3 deletions functions/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import (
"log"
"os"

"upload-token.functions"

"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"

functions "upload-token.functions"
"upload-token.functions/internal/util"
)

func main() {
funcframework.RegisterHTTPFunction("/challenge", functions.ChallengeHandler)
funcframework.RegisterHTTPFunction("/challenge", util.MakeTestHTTPHandler(functions.ChallengeHandler))
// Use PORT environment variable, or default to 8080.
port := "8080"
if envPort := os.Getenv("PORT"); envPort != "" {
Expand Down
3 changes: 2 additions & 1 deletion functions/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ require (
github.com/stretchr/testify v1.5.1
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect
golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c // indirect
google.golang.org/api v0.23.0 // indirect
google.golang.org/api v0.23.0
google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31 // indirect
google.golang.org/grpc v1.29.1
)
8 changes: 5 additions & 3 deletions functions/internal/pow/pow.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ const (
// Allow challenges to remain valid for one minute to allow for slow
// connections. We may need to increase this if we find that there's a tail
// of clients whose connections are bad enough that this is too short.
expirationPeriod = 60 * time.Second
defaultWorkFactor = 1024
expirationPeriod = 60 * time.Second
// DefaultWorkFactor is the default work factor used when generating
// challenges.
DefaultWorkFactor = 1024

// The name of the Firestore collection of challenges.
challengeCollection = "challenges"
Expand Down Expand Up @@ -148,7 +150,7 @@ type challengeDoc struct {

// GenerateChallenge generates a new challenge and stores it in the database.
func GenerateChallenge(ctx *util.Context) (*Challenge, error) {
c := generateChallenge(defaultWorkFactor)
c := generateChallenge(DefaultWorkFactor)

doc := challengeDoc{Expiration: time.Now().Add(expirationPeriod)}
_, err := ctx.FirestoreClient().Collection(challengeCollection).Doc(c.docID()).Create(ctx, doc)
Expand Down
4 changes: 2 additions & 2 deletions functions/internal/pow/pow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestValidate(t *testing.T) {

// On a 2018 MacBook Pro, this takes ~930us per validation.
func BenchmarkValidate(b *testing.B) {
c := generateChallenge(defaultWorkFactor)
c := generateChallenge(DefaultWorkFactor)
var s Solution
for {
_, err := rand.Read(s.inner.Nonce[:])
Expand All @@ -48,6 +48,6 @@ func BenchmarkValidate(b *testing.B) {
// On a 2018 MacBook Pro, this takes ~1100ns per validation.
func BenchmarkGenerate(b *testing.B) {
for i := 0; i < b.N; i++ {
generateChallenge(defaultWorkFactor)
generateChallenge(DefaultWorkFactor)
}
}
Loading

0 comments on commit c0ba7cb

Please sign in to comment.