-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[test] Add framework for running Firestore emulator for unit tests
- Loading branch information
Showing
11 changed files
with
286 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package functions | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"functions/internal/pow" | ||
"functions/internal/util" | ||
) | ||
|
||
func TestChallenge(t *testing.T) { | ||
firestore := util.NewTestFirestore(t) | ||
|
||
c := util.WithTestFirestore(context.Background(), firestore) | ||
req, err := http.NewRequestWithContext(c, "GET", "/challenge", nil) | ||
assert.Nil(t, err) | ||
r := httptest.NewRecorder() | ||
ctx, err := util.NewContext(r, req) | ||
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package util | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"context" | ||
"io" | ||
"log" | ||
"net" | ||
"os/exec" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"cloud.google.com/go/firestore" | ||
"google.golang.org/api/option" | ||
"google.golang.org/grpc" | ||
) | ||
|
||
// TestFirestore is a handle to a Firestore emulator running as a subprocess. | ||
// | ||
// When a TestFirestore is garbage collected, the subprocess is killed. Make | ||
// sure to keep a TestFirestore reachable so long as code is attempting to use | ||
// the emulator so the emulator won't be killed. | ||
type TestFirestore struct { | ||
// The emulator subprocess. | ||
emulator *exec.Cmd | ||
host string | ||
} | ||
|
||
func NewTestFirestore(t *testing.T) *TestFirestore { | ||
// For some reason, if the emulator crashes, the stderr does not close, and | ||
// without any other intervention, our stderr scanning code hangs | ||
// indefinitely. This timeout ensures that, if that happens, this timeout | ||
// will eventually trigger, the process will be killed (thus closing | ||
// stderr), and we will return an error. | ||
// | ||
// TODO(joshlf): Implement a more sophisticated solution to this problem. | ||
// The right way to do this is probably to spawn two goroutines - one to | ||
// scan stderr, and one to monitor the process to see if it quits. | ||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
defer cancel() | ||
|
||
// Do this separately ahead of time (rather than just passing "gcloud" to | ||
// exec.CommandContext) to give more precise errors. | ||
gcloudPath, err := exec.LookPath("gcloud") | ||
if err != nil { | ||
t.Fatalf("could not find gcloud: %v", err) | ||
} | ||
// When we don't specify a local address, a random port is chosen | ||
// automatically. | ||
cmd := exec.CommandContext(ctx, gcloudPath, "beta", "emulators", "firestore", "start") | ||
stderr, err := cmd.StderrPipe() | ||
if err != nil { | ||
t.Fatalf("could not start Firestore emulator: %v", err) | ||
} | ||
defer stderr.Close() | ||
|
||
if err := cmd.Start(); err != nil { | ||
t.Fatalf("could not start Firestore emulator: %v", err) | ||
} | ||
|
||
firestore := &TestFirestore{emulator: cmd} | ||
// Set a finalizer so that the subprocess is killed when we're done with it. | ||
runtime.SetFinalizer(firestore, func(firestore *TestFirestore) { | ||
err := firestore.emulator.Process.Kill() | ||
if err != nil { | ||
log.Print("could not kill emulator process:", err) | ||
} | ||
}) | ||
|
||
// Parse the command's stderr until we find the line indicating what local | ||
// address the emulator is listening on. Example output: | ||
// | ||
// Executing: /Users/dvader/google-cloud-sdk/platform/cloud-firestore-emulator/cloud_firestore_emulator start --host=::1 --port=8007 | ||
// [firestore] API endpoint: http://::1:8007 | ||
// [firestore] If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: | ||
// [firestore] | ||
// [firestore] export FIRESTORE_EMULATOR_HOST=::1:8007 | ||
// [firestore] | ||
// [firestore] Dev App Server is now running. | ||
// [firestore] | ||
// | ||
// In particular, we look for "export FIRESTORE_EMULATOR_HOST=". | ||
|
||
// Tee all of stderr to a buffer so we can provide more informative error | ||
// messages if need be. | ||
cached := bytes.NewBuffer(nil) | ||
s := bufio.NewScanner(io.TeeReader(stderr, cached)) | ||
var host string | ||
loop: | ||
for s.Scan() { | ||
parts := strings.Split(s.Text(), "export FIRESTORE_EMULATOR_HOST=") | ||
switch len(parts) { | ||
case 1: | ||
if strings.Contains(s.Text(), "Dev App Server is now running") { | ||
break loop | ||
} | ||
case 2: | ||
host = parts[1] | ||
default: | ||
t.Fatalf("got unexpected line from stderr output: \"%v\"", s.Text()) | ||
} | ||
} | ||
|
||
if err := s.Err(); err != nil { | ||
t.Fatalf("error reading output: %v; contents of stderr:\n%v", err, string(cached.Bytes())) | ||
} | ||
|
||
if host == "" { | ||
t.Fatalf("emulator started without outputting its listening address; contents of stderr:\n%s", string(cached.Bytes())) | ||
} | ||
|
||
// Instead of just storing the host as is, we split it into two and then | ||
// recombine it using net.JoinHostPort. The reason is that, when using IPv6, | ||
// the host looks like "::1:8007", which cannot be parsed as an IP | ||
// address/port pair by the Go Firestore client. net.JoinHostPort takes care | ||
// of wrapping IPv6 addresses in square brackets. | ||
colon := strings.LastIndex(host, ":") | ||
if colon == -1 { | ||
t.Fatalf("could not parse host: %v", host) | ||
} | ||
|
||
firestore.host = net.JoinHostPort(host[:colon], host[colon+1:]) | ||
return firestore | ||
} | ||
|
||
// FirestoreClient creates a *firestore.Client which connects to this Firestore. | ||
func (t *TestFirestore) FirestoreClient(ctx context.Context) (*firestore.Client, error) { | ||
opt, err := t.clientOption() | ||
if err != nil { | ||
return nil, err | ||
} | ||
return firestore.NewClient(ctx, "test", opt) | ||
} | ||
|
||
func (t *TestFirestore) clientOption() (option.ClientOption, error) { | ||
conn, err := grpc.Dial(t.host, grpc.WithInsecure(), grpc.WithPerRPCCredentials(emulatorCreds{})) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return option.WithGRPCConn(conn), nil | ||
} | ||
|
||
// emulatorCreds is taken from cloud.google.com/go/firestore/client.go. | ||
// | ||
// TODO(https://github.com/googleapis/google-cloud-go/issues/1978): Switch to a | ||
// first-class API if one is provided. | ||
|
||
// emulatorCreds is an instance of grpc.PerRPCCredentials that will configure a | ||
// client to act as an admin for the Firestore emulator. It always hardcodes | ||
// the "authorization" metadata field to contain "Bearer owner", which the | ||
// Firestore emulator accepts as valid admin credentials. | ||
type emulatorCreds struct{} | ||
|
||
func (ec emulatorCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { | ||
return map[string]string{"authorization": "Bearer owner"}, nil | ||
} | ||
|
||
func (ec emulatorCreds) RequireTransportSecurity() bool { | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package util | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestNewFirestore(t *testing.T) { | ||
f := NewTestFirestore(t) | ||
assert.NotEqual(t, f.host, "") | ||
} |
Oops, something went wrong.