Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
54b8114
Merge dev into master
google-oss-bot May 21, 2020
cef91ac
Merge dev into master
google-oss-bot Jun 16, 2020
77177c7
Merge dev into master
google-oss-bot Oct 22, 2020
a957589
Merge dev into master
google-oss-bot Jan 28, 2021
eb0d2a0
Merge dev into master
google-oss-bot Mar 24, 2021
05378ef
Merge dev into master
google-oss-bot Mar 29, 2021
4121c50
Merge dev into master
google-oss-bot Apr 14, 2021
928b104
Merge dev into master
google-oss-bot Jun 2, 2021
02cde4f
Merge dev into master
google-oss-bot Nov 4, 2021
6b40682
Merge dev into master
google-oss-bot Dec 15, 2021
e60757f
Merge dev into master
google-oss-bot Jan 20, 2022
bb055ed
Merge dev into master
google-oss-bot Apr 6, 2022
23a1f17
Merge dev into master
google-oss-bot Oct 6, 2022
21d7d61
Comment on what work needs to be done
r-LaForge Oct 14, 2022
fda4e90
[rtdb emulator]: add the emulator url as an option
r-LaForge Oct 14, 2022
2d92931
[rtdb emulator]: use an emulator token source for the transport
r-LaForge Oct 14, 2022
12e6a60
[test]: Fix the unit tests
r-LaForge Oct 14, 2022
05f172b
[test]: More tests for when there is a subdomain included
r-LaForge Oct 14, 2022
3d95aa6
[chore]: cleanup comment
r-LaForge Oct 14, 2022
9cce22e
[chore]: make the db test work with the emulator as well
r-LaForge Oct 14, 2022
dcac128
[chore]: minor patch
r-LaForge Oct 14, 2022
e3e2077
[chore]: update port for emulator in comment
r-LaForge Oct 14, 2022
1e8716c
[fix]: Account for the environment variable
r-LaForge Oct 14, 2022
18519f1
[chore]: cleanup a variable name
r-LaForge Oct 14, 2022
ece67c0
[chore]: tidy
r-LaForge Oct 15, 2022
d611de6
Update db/db.go
r-LaForge Oct 16, 2022
22b76d3
lint: fix lint on ErrInvalidURL
r-LaForge Oct 18, 2022
c0078c2
make ErrInvalidURL not public
r-LaForge Oct 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 95 additions & 6 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,56 @@ package db
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"runtime"
"strings"

"firebase.google.com/go/v4/internal"
"golang.org/x/oauth2"
"google.golang.org/api/option"
)

const userAgentFormat = "Firebase/HTTP/%s/%s/AdminGo"
const invalidChars = "[].#$"
const authVarOverride = "auth_variable_override"
const emulatorDatabaseEnvVar = "FIREBASE_DATABASE_EMULATOR_HOST"
const emulatorNamespaceParam = "ns"

var ErrInvalidURL = errors.New("invalid database url")

var emulatorToken = &oauth2.Token{
AccessToken: "owner",
}

// Client is the interface for the Firebase Realtime Database service.
type Client struct {
hc *internal.HTTPClient
url string
dbURLConfig *dbURLConfig
authOverride string
}

type dbURLConfig struct {
// BaseURL can be either:
// - a production url (https://foo-bar.firebaseio.com/)
// - an emulator url (http://localhost:9000)
BaseURL string

// Namespace is used in for the emulator to specify the databaseName
// To specify a namespace on your url, pass ns=<database_name> (localhost:9000/?ns=foo-bar)
Namespace string
}

// NewClient creates a new instance of the Firebase Database Client.
//
// This function can only be invoked from within the SDK. Client applications should access the
// Database service through firebase.App.
func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) {
p, err := url.ParseRequestURI(c.URL)
urlConfig, isEmulator, err := parseURLConfig(c.URL)
if err != nil {
return nil, err
} else if p.Scheme != "https" {
return nil, fmt.Errorf("invalid database URL: %q; want scheme: %q", c.URL, "https")
}

var ao []byte
Expand All @@ -59,6 +79,10 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error)
}

opts := append([]option.ClientOption{}, c.Opts...)
if isEmulator {
ts := oauth2.StaticTokenSource(emulatorToken)
opts = append(opts, option.WithTokenSource(ts))
}
ua := fmt.Sprintf(userAgentFormat, c.Version, runtime.Version())
opts = append(opts, option.WithUserAgent(ua))
hc, _, err := internal.NewHTTPClient(ctx, opts...)
Expand All @@ -69,7 +93,7 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error)
hc.CreateErrFn = handleRTDBError
return &Client{
hc: hc,
url: fmt.Sprintf("https://%s", p.Host),
dbURLConfig: urlConfig,
authOverride: string(ao),
}, nil
}
Expand All @@ -96,10 +120,13 @@ func (c *Client) sendAndUnmarshal(
return nil, fmt.Errorf("invalid path with illegal characters: %q", req.URL)
}

req.URL = fmt.Sprintf("%s%s.json", c.url, req.URL)
req.URL = fmt.Sprintf("%s%s.json", c.dbURLConfig.BaseURL, req.URL)
if c.authOverride != "" {
req.Opts = append(req.Opts, internal.WithQueryParam(authVarOverride, c.authOverride))
}
if c.dbURLConfig.Namespace != "" {
req.Opts = append(req.Opts, internal.WithQueryParam(emulatorNamespaceParam, c.dbURLConfig.Namespace))
}

return c.hc.DoAndUnmarshal(ctx, req, v)
}
Expand All @@ -126,3 +153,65 @@ func handleRTDBError(resp *internal.Response) error {

return err
}

// parseURLConfig returns the dbURLConfig for the database
// dbURL may be either:
// - a production url (https://foo-bar.firebaseio.com/)
// - an emulator URL (localhost:9000/?ns=foo-bar)
//
// The following rules will apply for determining the output:
// - If the url has no scheme it will be assumed to be an emulator url and be used.
// - else If the FIREBASE_DATABASE_EMULATOR_HOST environment variable is set it will be used.
// - else the url will be assumed to be a production url and be used.
func parseURLConfig(dbURL string) (*dbURLConfig, bool, error) {
parsedURL, err := url.ParseRequestURI(dbURL)
if err == nil && parsedURL.Scheme != "https" {
cfg, err := parseEmulatorHost(dbURL, parsedURL)
return cfg, true, err
}

environmentEmulatorURL := os.Getenv(emulatorDatabaseEnvVar)
if environmentEmulatorURL == "" && err != nil {
return nil, false, fmt.Errorf("%s: %w", dbURL, ErrInvalidURL)
}

if environmentEmulatorURL == "" && err == nil {
return &dbURLConfig{
BaseURL: dbURL,
Namespace: "",
}, false, nil
}

parsedURL, err = url.ParseRequestURI(environmentEmulatorURL)
if err != nil {
return nil, false, fmt.Errorf("%s: %w", environmentEmulatorURL, ErrInvalidURL)
}
cfg, err := parseEmulatorHost(environmentEmulatorURL, parsedURL)
return cfg, true, err
}

func parseEmulatorHost(rawEmulatorHostURL string, parsedEmulatorHost *url.URL) (*dbURLConfig, error) {
if strings.Contains(rawEmulatorHostURL, "//") {
Comment on lines +195 to +196
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil, fmt.Errorf(`invalid %s: "%s". It must follow format "host:port": %w`, emulatorDatabaseEnvVar, rawEmulatorHostURL, ErrInvalidURL)
}

baseURL := strings.Replace(rawEmulatorHostURL, fmt.Sprintf("?%s", parsedEmulatorHost.RawQuery), "", -1)
if parsedEmulatorHost.Scheme != "http" {
baseURL = fmt.Sprintf("http://%s", baseURL)
}

namespace := parsedEmulatorHost.Query().Get(emulatorNamespaceParam)
if namespace == "" {
if strings.Contains(rawEmulatorHostURL, ".") {
namespace = strings.Split(rawEmulatorHostURL, ".")[0]
}
Comment on lines +207 to +209
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if namespace == "" {
return nil, fmt.Errorf(`invalid database URL: "%s". Database URL must be a valid URL to a Firebase Realtime Database instance (include ?ns=<db-name> query param)`, parsedEmulatorHost)
}
}

return &dbURLConfig{
BaseURL: baseURL,
Namespace: namespace,
}, nil
}
134 changes: 91 additions & 43 deletions db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ import (
)

const (
testURL = "https://test-db.firebaseio.com"
defaultMaxRetries = 1
testURL = "https://test-db.firebaseio.com"
testEmulatorNamespace = "test-db"
testEmulatorBaseURL = "http://localhost:9000"
testEmulatorURL = "localhost:9000?ns=test-db"
defaultMaxRetries = 1
)

var (
Expand Down Expand Up @@ -87,52 +90,96 @@ func TestMain(m *testing.M) {
}

func TestNewClient(t *testing.T) {
c, err := NewClient(context.Background(), &internal.DatabaseConfig{
Opts: testOpts,
URL: testURL,
AuthOverride: make(map[string]interface{}),
})
if err != nil {
t.Fatal(err)
}
if c.url != testURL {
t.Errorf("NewClient().url = %q; want = %q", c.url, testURL)
}
if c.hc == nil {
t.Errorf("NewClient().hc = nil; want non-nil")
cases := []*struct {
Name string
URL string
EnvURL string
ExpectedBaseURL string
ExpectedNamespace string
ExpectError bool
}{
{Name: "production url", URL: testURL, ExpectedBaseURL: testURL, ExpectedNamespace: ""},
{Name: "emulator - success", URL: testEmulatorURL, ExpectedBaseURL: testEmulatorBaseURL, ExpectedNamespace: testEmulatorNamespace},
{Name: "emulator - missing namespace should error", URL: "localhost:9000", ExpectError: true},
{Name: "emulator - if url contains hostname it uses the primary domain", URL: "rtdb-go.emulator:9000", ExpectedBaseURL: "http://rtdb-go.emulator:9000", ExpectedNamespace: "rtdb-go"},
{Name: "emulator env - success", EnvURL: testEmulatorURL, ExpectedBaseURL: testEmulatorBaseURL, ExpectedNamespace: testEmulatorNamespace},
}
if c.authOverride != "" {
t.Errorf("NewClient().ao = %q; want = %q", c.authOverride, "")
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
t.Setenv(emulatorDatabaseEnvVar, tc.EnvURL)
fromEnv := os.Getenv(emulatorDatabaseEnvVar)
fmt.Printf(fromEnv)
c, err := NewClient(context.Background(), &internal.DatabaseConfig{
Opts: testOpts,
URL: tc.URL,
AuthOverride: make(map[string]interface{}),
})
if err != nil && tc.ExpectError {
return
}
if err != nil && !tc.ExpectError {
t.Fatal(err)
}
if err == nil && tc.ExpectError {
t.Fatal("expected error")
}
if c.dbURLConfig.BaseURL != tc.ExpectedBaseURL {
t.Errorf("NewClient().dbURLConfig.BaseURL = %q; want = %q", c.dbURLConfig.BaseURL, tc.ExpectedBaseURL)
}
if c.dbURLConfig.Namespace != tc.ExpectedNamespace {
t.Errorf("NewClient(%v).Namespace = %q; want = %q", tc, c.dbURLConfig.Namespace, tc.ExpectedNamespace)
}
if c.hc == nil {
t.Errorf("NewClient().hc = nil; want non-nil")
}
if c.authOverride != "" {
t.Errorf("NewClient().ao = %q; want = %q", c.authOverride, "")
}
})
}
}

func TestNewClientAuthOverrides(t *testing.T) {
cases := []map[string]interface{}{
nil,
{"uid": "user1"},
cases := []*struct {
Name string
Params map[string]interface{}
URL string
ExpectedBaseURL string
ExpectedNamespace string
}{
{Name: "production - without override", Params: nil, URL: testURL, ExpectedBaseURL: testURL, ExpectedNamespace: ""},
{Name: "production - with override", Params: map[string]interface{}{"uid": "user1"}, URL: testURL, ExpectedBaseURL: testURL, ExpectedNamespace: ""},

{Name: "emulator - with no query params", Params: nil, URL: testEmulatorURL, ExpectedBaseURL: testEmulatorBaseURL, ExpectedNamespace: testEmulatorNamespace},
{Name: "emulator - with override", Params: map[string]interface{}{"uid": "user1"}, URL: testEmulatorURL, ExpectedBaseURL: testEmulatorBaseURL, ExpectedNamespace: testEmulatorNamespace},
}
for _, tc := range cases {
c, err := NewClient(context.Background(), &internal.DatabaseConfig{
Opts: testOpts,
URL: testURL,
AuthOverride: tc,
t.Run(tc.Name, func(t *testing.T) {
c, err := NewClient(context.Background(), &internal.DatabaseConfig{
Opts: testOpts,
URL: tc.URL,
AuthOverride: tc.Params,
})
if err != nil {
t.Fatal(err)
}
if c.dbURLConfig.BaseURL != tc.ExpectedBaseURL {
t.Errorf("NewClient(%v).baseURL = %q; want = %q", tc, c.dbURLConfig.BaseURL, tc.ExpectedBaseURL)
}
if c.dbURLConfig.Namespace != tc.ExpectedNamespace {
t.Errorf("NewClient(%v).Namespace = %q; want = %q", tc, c.dbURLConfig.Namespace, tc.ExpectedNamespace)
}
if c.hc == nil {
t.Errorf("NewClient(%v).hc = nil; want non-nil", tc)
}
b, err := json.Marshal(tc.Params)
if err != nil {
t.Fatal(err)
}
if c.authOverride != string(b) {
t.Errorf("NewClient(%v).ao = %q; want = %q", tc, c.authOverride, string(b))
}
})
if err != nil {
t.Fatal(err)
}
if c.url != testURL {
t.Errorf("NewClient(%v).url = %q; want = %q", tc, c.url, testURL)
}
if c.hc == nil {
t.Errorf("NewClient(%v).hc = nil; want non-nil", tc)
}
b, err := json.Marshal(tc)
if err != nil {
t.Fatal(err)
}
if c.authOverride != string(b) {
t.Errorf("NewClient(%v).ao = %q; want = %q", tc, c.authOverride, string(b))
}
}
}

Expand All @@ -149,8 +196,8 @@ func TestValidURLS(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if c.url != tc {
t.Errorf("NewClient(%v).url = %q; want = %q", tc, c.url, testURL)
if c.dbURLConfig.BaseURL != tc {
t.Errorf("NewClient(%v).url = %q; want = %q", tc, c.dbURLConfig.BaseURL, testURL)
}
}
}
Expand All @@ -161,6 +208,7 @@ func TestInvalidURL(t *testing.T) {
"foo",
"http://db.firebaseio.com",
"http://firebase.google.com",
"http://localhost:9000",
}
for _, tc := range cases {
c, err := NewClient(context.Background(), &internal.DatabaseConfig{
Expand Down Expand Up @@ -402,7 +450,7 @@ func (s *mockServer) Start(c *Client) *httptest.Server {
w.Write(b)
})
s.srv = httptest.NewServer(handler)
c.url = s.srv.URL
c.dbURLConfig.BaseURL = s.srv.URL
return s.srv
}

Expand Down
Loading