Skip to content

Commit f4d07f6

Browse files
authored
Firebase App auto init (#54)
* FIREBASE_CONFIG env variable auto init, allows calling initialize_app with not arguments using the env variable * allow env var to contain json * add tests for json string in env file
1 parent eccb159 commit f4d07f6

7 files changed

+226
-15
lines changed

firebase.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
package firebase
1919

2020
import (
21+
"encoding/json"
2122
"errors"
23+
"io/ioutil"
24+
"os"
2225

2326
"cloud.google.com/go/firestore"
2427

@@ -27,8 +30,6 @@ import (
2730
"firebase.google.com/go/internal"
2831
"firebase.google.com/go/storage"
2932

30-
"os"
31-
3233
"golang.org/x/net/context"
3334
"golang.org/x/oauth2/google"
3435
"google.golang.org/api/option"
@@ -47,6 +48,9 @@ var firebaseScopes = []string{
4748
// Version of the Firebase Go Admin SDK.
4849
const Version = "2.3.0"
4950

51+
// firebaseEnvName is the name of the environment variable with the Config.
52+
const firebaseEnvName = "FIREBASE_CONFIG"
53+
5054
// An App holds configuration and state common to all Firebase services that are exposed from the SDK.
5155
type App struct {
5256
creds *google.DefaultCredentials
@@ -57,8 +61,8 @@ type App struct {
5761

5862
// Config represents the configuration used to initialize an App.
5963
type Config struct {
60-
ProjectID string
61-
StorageBucket string
64+
ProjectID string `json:"projectId"`
65+
StorageBucket string `json:"storageBucket"`
6266
}
6367

6468
// Auth returns an instance of auth.Client.
@@ -107,14 +111,14 @@ func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) {
107111
func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (*App, error) {
108112
o := []option.ClientOption{option.WithScopes(firebaseScopes...)}
109113
o = append(o, opts...)
110-
111114
creds, err := transport.Creds(ctx, o...)
112115
if err != nil {
113116
return nil, err
114117
}
115-
116118
if config == nil {
117-
config = &Config{}
119+
if config, err = getConfigDefaults(); err != nil {
120+
return nil, err
121+
}
118122
}
119123

120124
var pid string
@@ -133,3 +137,24 @@ func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (*
133137
opts: o,
134138
}, nil
135139
}
140+
141+
// getConfigDefaults reads the default config file, defined by the FIREBASE_CONFIG
142+
// env variable, used only when options are nil.
143+
func getConfigDefaults() (*Config, error) {
144+
fbc := &Config{}
145+
confFileName := os.Getenv(firebaseEnvName)
146+
if confFileName == "" {
147+
return fbc, nil
148+
}
149+
var dat []byte
150+
if confFileName[0] == byte('{') {
151+
dat = []byte(confFileName)
152+
} else {
153+
var err error
154+
if dat, err = ioutil.ReadFile(confFileName); err != nil {
155+
return nil, err
156+
}
157+
}
158+
err := json.Unmarshal(dat, fbc)
159+
return fbc, err
160+
}

firebase_test.go

Lines changed: 182 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package firebase
1616

1717
import (
1818
"io/ioutil"
19+
"log"
1920
"net/http"
2021
"net/http/httptest"
2122
"os"
@@ -35,6 +36,17 @@ import (
3536
"google.golang.org/api/option"
3637
)
3738

39+
const credEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
40+
41+
func TestMain(m *testing.M) {
42+
// This isolates the tests from a possiblity that the default config env
43+
// variable is set to a valid file containing the wanted default config,
44+
// but we the test is not expecting it.
45+
configOld := overwriteEnv(firebaseEnvName, "")
46+
defer reinstateEnv(firebaseEnvName, configOld)
47+
os.Exit(m.Run())
48+
}
49+
3850
func TestServiceAcctFile(t *testing.T) {
3951
app, err := NewApp(context.Background(), nil, option.WithCredentialsFile("testdata/service_account.json"))
4052
if err != nil {
@@ -151,13 +163,12 @@ func TestRefreshTokenWithEnvVar(t *testing.T) {
151163
}
152164

153165
func TestAppDefault(t *testing.T) {
154-
varName := "GOOGLE_APPLICATION_CREDENTIALS"
155-
current := os.Getenv(varName)
166+
current := os.Getenv(credEnvVar)
156167

157-
if err := os.Setenv(varName, "testdata/service_account.json"); err != nil {
168+
if err := os.Setenv(credEnvVar, "testdata/service_account.json"); err != nil {
158169
t.Fatal(err)
159170
}
160-
defer os.Setenv(varName, current)
171+
defer os.Setenv(credEnvVar, current)
161172

162173
app, err := NewApp(context.Background(), nil)
163174
if err != nil {
@@ -175,13 +186,12 @@ func TestAppDefault(t *testing.T) {
175186
}
176187

177188
func TestAppDefaultWithInvalidFile(t *testing.T) {
178-
varName := "GOOGLE_APPLICATION_CREDENTIALS"
179-
current := os.Getenv(varName)
189+
current := os.Getenv(credEnvVar)
180190

181-
if err := os.Setenv(varName, "testdata/non_existing.json"); err != nil {
191+
if err := os.Setenv(credEnvVar, "testdata/non_existing.json"); err != nil {
182192
t.Fatal(err)
183193
}
184-
defer os.Setenv(varName, current)
194+
defer os.Setenv(credEnvVar, current)
185195

186196
app, err := NewApp(context.Background(), nil)
187197
if app != nil || err == nil {
@@ -337,6 +347,139 @@ func TestVersion(t *testing.T) {
337347
}
338348
}
339349
}
350+
func TestAutoInit(t *testing.T) {
351+
tests := []struct {
352+
name string
353+
optionsConfig string
354+
initOptions *Config
355+
wantOptions *Config
356+
}{
357+
{
358+
"No environment variable, no explicit options",
359+
"",
360+
nil,
361+
&Config{ProjectID: "mock-project-id"}, // from default creds here and below.
362+
}, {
363+
"Environment variable set to file, no explicit options",
364+
"testdata/firebase_config.json",
365+
nil,
366+
&Config{
367+
ProjectID: "hipster-chat-mock",
368+
StorageBucket: "hipster-chat.appspot.mock",
369+
},
370+
}, {
371+
"Environment variable set to string, no explicit options",
372+
`{
373+
"projectId": "hipster-chat-mock",
374+
"storageBucket": "hipster-chat.appspot.mock"
375+
}`,
376+
nil,
377+
&Config{
378+
ProjectID: "hipster-chat-mock",
379+
StorageBucket: "hipster-chat.appspot.mock",
380+
},
381+
}, {
382+
"Environment variable set to file with some values missing, no explicit options",
383+
"testdata/firebase_config_partial.json",
384+
nil,
385+
&Config{ProjectID: "hipster-chat-mock"},
386+
}, {
387+
"Environment variable set to string with some values missing, no explicit options",
388+
`{"projectId": "hipster-chat-mock"}`,
389+
nil,
390+
&Config{ProjectID: "hipster-chat-mock"},
391+
}, {
392+
"Environment variable set to file which is ignored as some explicit options are passed",
393+
"testdata/firebase_config_partial.json",
394+
&Config{StorageBucket: "sb1-mock"},
395+
&Config{
396+
ProjectID: "mock-project-id",
397+
StorageBucket: "sb1-mock",
398+
},
399+
}, {
400+
"Environment variable set to string which is ignored as some explicit options are passed",
401+
`{"projectId": "hipster-chat-mock"}`,
402+
&Config{StorageBucket: "sb1-mock"},
403+
&Config{
404+
ProjectID: "mock-project-id",
405+
StorageBucket: "sb1-mock",
406+
},
407+
}, {
408+
"Environment variable set to file which is ignored as options are explicitly empty",
409+
"testdata/firebase_config_partial.json",
410+
&Config{},
411+
&Config{ProjectID: "mock-project-id"},
412+
}, {
413+
"Environment variable set to file with an unknown key which is ignored, no explicit options",
414+
"testdata/firebase_config_invalid_key.json",
415+
nil,
416+
&Config{
417+
ProjectID: "mock-project-id", // from default creds
418+
StorageBucket: "hipster-chat.appspot.mock",
419+
},
420+
}, {
421+
"Environment variable set to string with an unknown key which is ignored, no explicit options",
422+
`{
423+
"obviously_bad_key": "hipster-chat-mock",
424+
"storageBucket": "hipster-chat.appspot.mock"
425+
}`,
426+
nil,
427+
&Config{
428+
ProjectID: "mock-project-id",
429+
StorageBucket: "hipster-chat.appspot.mock",
430+
},
431+
},
432+
}
433+
434+
credOld := overwriteEnv(credEnvVar, "testdata/service_account.json")
435+
defer reinstateEnv(credEnvVar, credOld)
436+
437+
for _, test := range tests {
438+
t.Run(test.name, func(t *testing.T) {
439+
overwriteEnv(firebaseEnvName, test.optionsConfig)
440+
app, err := NewApp(context.Background(), test.initOptions)
441+
if err != nil {
442+
t.Error(err)
443+
} else {
444+
compareConfig(app, test.wantOptions, t)
445+
}
446+
})
447+
}
448+
}
449+
450+
func TestAutoInitInvalidFiles(t *testing.T) {
451+
tests := []struct {
452+
name string
453+
filename string
454+
wantError string
455+
}{
456+
{
457+
"nonexistant file",
458+
"testdata/no_such_file.json",
459+
"open testdata/no_such_file.json: no such file or directory",
460+
}, {
461+
"invalid JSON",
462+
"testdata/firebase_config_invalid.json",
463+
"invalid character 'b' looking for beginning of value",
464+
}, {
465+
"empty file",
466+
"testdata/firebase_config_empty.json",
467+
"unexpected end of JSON input",
468+
},
469+
}
470+
credOld := overwriteEnv(credEnvVar, "testdata/service_account.json")
471+
defer reinstateEnv(credEnvVar, credOld)
472+
473+
for _, test := range tests {
474+
t.Run(test.name, func(t *testing.T) {
475+
overwriteEnv(firebaseEnvName, test.filename)
476+
_, err := NewApp(context.Background(), nil)
477+
if err == nil || err.Error() != test.wantError {
478+
t.Errorf("got error = %s; want = %s", err, test.wantError)
479+
}
480+
})
481+
}
482+
}
340483

341484
type testTokenSource struct {
342485
AccessToken string
@@ -350,6 +493,15 @@ func (t *testTokenSource) Token() (*oauth2.Token, error) {
350493
}, nil
351494
}
352495

496+
func compareConfig(got *App, want *Config, t *testing.T) {
497+
if got.projectID != want.ProjectID {
498+
t.Errorf("app.projectID = %q; want = %q", got.projectID, want.ProjectID)
499+
}
500+
if got.storageBucket != want.StorageBucket {
501+
t.Errorf("app.storageBucket = %q; want = %q", got.storageBucket, want.StorageBucket)
502+
}
503+
}
504+
353505
// mockServiceAcct generates a service account configuration with the provided URL as the
354506
// token_url value.
355507
func mockServiceAcct(tokenURL string) ([]byte, error) {
@@ -379,3 +531,25 @@ func initMockTokenServer() *httptest.Server {
379531
}`))
380532
}))
381533
}
534+
535+
// overwriteEnv overwrites env variables, used in testsing.
536+
func overwriteEnv(varName, newVal string) string {
537+
oldVal := os.Getenv(varName)
538+
if newVal == "" {
539+
if err := os.Unsetenv(varName); err != nil {
540+
log.Fatal(err)
541+
}
542+
} else if err := os.Setenv(varName, newVal); err != nil {
543+
log.Fatal(err)
544+
}
545+
return oldVal
546+
}
547+
548+
// reinstateEnv restores the enviornment variable, will usually be used deferred with overwriteEnv.
549+
func reinstateEnv(varName, oldVal string) {
550+
if len(varName) > 0 {
551+
os.Setenv(varName, oldVal)
552+
} else {
553+
os.Unsetenv(varName)
554+
}
555+
}

testdata/firebase_config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"projectId": "hipster-chat-mock",
3+
"storageBucket": "hipster-chat.appspot.mock"
4+
}

testdata/firebase_config_empty.json

Whitespace-only changes.

testdata/firebase_config_invalid.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
baaad
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"project1d_bad_key": "hipster-chat-mock",
3+
"storageBucket": "hipster-chat.appspot.mock"
4+
}

testdata/firebase_config_partial.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"projectId": "hipster-chat-mock"
3+
}

0 commit comments

Comments
 (0)