From dc51231df8cc82f663fffbf60d2b765fe5209ab0 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 16 Aug 2024 12:51:30 +0100 Subject: [PATCH 1/4] feat: add Init method to firebase go plugin, remove custom app struct --- go/plugins/firebase/auth_test.go | 1 + go/plugins/firebase/firebase.go | 74 ++++++++++++++----- go/plugins/firebase/firebase_app_test.go | 77 ++++++++++++++++++++ go/plugins/firebase/firebase_init_test.go | 86 +++++++++++++++++++++++ 4 files changed, 220 insertions(+), 18 deletions(-) create mode 100644 go/plugins/firebase/firebase_app_test.go create mode 100644 go/plugins/firebase/firebase_init_test.go diff --git a/go/plugins/firebase/auth_test.go b/go/plugins/firebase/auth_test.go index 6bd566064d..3af9a79f85 100644 --- a/go/plugins/firebase/auth_test.go +++ b/go/plugins/firebase/auth_test.go @@ -93,6 +93,7 @@ func TestProvideAuthContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockClient := &mockAuthClient{ verifyIDTokenFunc: func(ctx context.Context, token string) (*auth.Token, error) { if token == "validtoken" { diff --git a/go/plugins/firebase/firebase.go b/go/plugins/firebase/firebase.go index e2f4a85b4d..5a94e6964a 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -16,31 +16,69 @@ package firebase import ( "context" + "fmt" + "log" "sync" firebase "firebase.google.com/go/v4" - "firebase.google.com/go/v4/auth" ) -type FirebaseApp interface { - Auth(ctx context.Context) (*auth.Client, error) +var state struct { + mu sync.Mutex + initted bool + app *firebase.App } -var ( - app *firebase.App - mutex sync.Mutex -) +// FirebasePluginConfig is the configuration for the Firebase plugin. +type FirebasePluginConfig struct { + AuthOverride *map[string]interface{} `json:"databaseAuthVariableOverride"` + DatabaseURL string `json:"databaseURL"` + ProjectID string `json:"projectId"` + ServiceAccountID string `json:"serviceAccountId"` + StorageBucket string `json:"storageBucket"` +} + +// Init initializes the Firebase app with the provided configuration. +// If called more than once, it logs a message and returns nil. +func Init(ctx context.Context, cfg *FirebasePluginConfig) error { + state.mu.Lock() + defer state.mu.Unlock() + + if state.initted { + log.Println("firebase.Init: already called, returning without reinitializing") + return nil + } + + // Prepare the Firebase config + firebaseConfig := &firebase.Config{ + AuthOverride: cfg.AuthOverride, + DatabaseURL: cfg.DatabaseURL, + ProjectID: cfg.ProjectID, // Allow ProjectID to be empty + ServiceAccountID: cfg.ServiceAccountID, + StorageBucket: cfg.StorageBucket, + } -// app returns a cached Firebase app. -func App(ctx context.Context) (FirebaseApp, error) { - mutex.Lock() - defer mutex.Unlock() - if app == nil { - newApp, err := firebase.NewApp(ctx, nil) - if err != nil { - return nil, err - } - app = newApp + // Initialize Firebase app with service account key if provided + app, err := firebase.NewApp(ctx, firebaseConfig) + if err != nil { + return fmt.Errorf("firebase.Init: %w", err) } - return app, nil + + state.app = app + state.initted = true + + return nil +} + +// App returns a cached Firebase app. +// If the app is not initialized, it returns an error. +func App(ctx context.Context) (*firebase.App, error) { + state.mu.Lock() + defer state.mu.Unlock() + + if !state.initted { + return nil, fmt.Errorf("firebase.App: Firebase app not initialized. Call Init first") + } + + return state.app, nil } diff --git a/go/plugins/firebase/firebase_app_test.go b/go/plugins/firebase/firebase_app_test.go new file mode 100644 index 0000000000..19528c49e8 --- /dev/null +++ b/go/plugins/firebase/firebase_app_test.go @@ -0,0 +1,77 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firebase + +import ( + "context" + "testing" +) + +func TestApp(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + setup func() error + expectedError string + }{ + { + name: "Get App before initialization", + setup: func() error { + // No initialization setup here, calling App directly should fail + return nil + }, + expectedError: "firebase.App: Firebase app not initialized. Call Init first", + }, + { + name: "Get App after successful initialization", + setup: func() error { + // Properly initialize the app + config := &FirebasePluginConfig{ + ProjectID: *firebaseProjectID, + } + return Init(ctx, config) + }, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Execute setup + if err := tt.setup(); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + // Now test the App function + app, err := App(ctx) + + if tt.expectedError != "" { + if err == nil || err.Error() != tt.expectedError { + t.Errorf("Expected error %q, got %v", tt.expectedError, err) + } + if app != nil { + t.Errorf("Expected no app, got %v", app) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) + } else if app == nil { + t.Errorf("Expected a valid app instance, got nil") + } + }) + } +} diff --git a/go/plugins/firebase/firebase_init_test.go b/go/plugins/firebase/firebase_init_test.go new file mode 100644 index 0000000000..af3f7bc64d --- /dev/null +++ b/go/plugins/firebase/firebase_init_test.go @@ -0,0 +1,86 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package firebase + +import ( + "context" + "flag" + "testing" +) + +// Define the flag with a default value of "demo-test" +var firebaseProjectID = flag.String("firebase-project-id", "demo-test", "Firebase project ID") + +func TestInit(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + config *FirebasePluginConfig + expectedError string + setup func() error + }{ + { + name: "Successful initialization", + config: &FirebasePluginConfig{ + ProjectID: *firebaseProjectID, + }, + expectedError: "", + setup: func() error { + return nil // No setup required, first call should succeed + }, + }, + { + name: "Initialization when already initialized", + config: &FirebasePluginConfig{ + ProjectID: *firebaseProjectID, + }, + expectedError: "", + setup: func() error { + return Init(ctx, &FirebasePluginConfig{ProjectID: *firebaseProjectID}) // Initialize once + }, + }, + { + name: "Initialization with missing ProjectID", + config: &FirebasePluginConfig{ + ProjectID: "", + }, + expectedError: "", // No error expected, as ProjectID can be inferred + setup: func() error { + return nil // No setup required + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.setup(); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + err := Init(ctx, tt.config) + + if tt.expectedError != "" { + if err == nil || err.Error() != tt.expectedError { + t.Errorf("Expected error %q, got %v", tt.expectedError, err) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) + } + }) + } +} From 425e68c88f61530e039b4b503a65423db9a176f1 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 19 Aug 2024 10:02:21 +0100 Subject: [PATCH 2/4] fix(go): make init logic deterministic in firebase go plugin --- go/plugins/firebase/firebase.go | 15 ++++++++++++--- go/plugins/firebase/firebase_init_test.go | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/go/plugins/firebase/firebase.go b/go/plugins/firebase/firebase.go index 5a94e6964a..e25ac5952a 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -17,7 +17,6 @@ package firebase import ( "context" "fmt" - "log" "sync" firebase "firebase.google.com/go/v4" @@ -45,8 +44,8 @@ func Init(ctx context.Context, cfg *FirebasePluginConfig) error { defer state.mu.Unlock() if state.initted { - log.Println("firebase.Init: already called, returning without reinitializing") - return nil + // throw an error if the app is already initialized + return fmt.Errorf("firebase.Init: Firebase app already initialized") } // Prepare the Firebase config @@ -70,6 +69,16 @@ func Init(ctx context.Context, cfg *FirebasePluginConfig) error { return nil } +// UnInit uninitializes the Firebase app. + +func UnInit() { + state.mu.Lock() + defer state.mu.Unlock() + + state.initted = false + state.app = nil +} + // App returns a cached Firebase app. // If the app is not initialized, it returns an error. func App(ctx context.Context) (*firebase.App, error) { diff --git a/go/plugins/firebase/firebase_init_test.go b/go/plugins/firebase/firebase_init_test.go index af3f7bc64d..7381187f5d 100644 --- a/go/plugins/firebase/firebase_init_test.go +++ b/go/plugins/firebase/firebase_init_test.go @@ -24,7 +24,6 @@ import ( var firebaseProjectID = flag.String("firebase-project-id", "demo-test", "Firebase project ID") func TestInit(t *testing.T) { - t.Parallel() ctx := context.Background() @@ -49,7 +48,7 @@ func TestInit(t *testing.T) { config: &FirebasePluginConfig{ ProjectID: *firebaseProjectID, }, - expectedError: "", + expectedError: "firebase.Init: Firebase app already initialized", setup: func() error { return Init(ctx, &FirebasePluginConfig{ProjectID: *firebaseProjectID}) // Initialize once }, @@ -68,6 +67,8 @@ func TestInit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer UnInit() + if err := tt.setup(); err != nil { t.Fatalf("Setup failed: %v", err) } From af854ef10ffea02dd3603b14454e71d599be5dc6 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:11:02 +0100 Subject: [PATCH 3/4] Update go/plugins/firebase/firebase.go Co-authored-by: Alex Pascal --- go/plugins/firebase/firebase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/plugins/firebase/firebase.go b/go/plugins/firebase/firebase.go index e25ac5952a..c5b2c94921 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -45,7 +45,7 @@ func Init(ctx context.Context, cfg *FirebasePluginConfig) error { if state.initted { // throw an error if the app is already initialized - return fmt.Errorf("firebase.Init: Firebase app already initialized") + return errors.New("firebase.Init: Firebase app already initialized") } // Prepare the Firebase config From e3712d0d58712d2c5d19b89580cf123cb58be2b5 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 21 Aug 2024 10:15:15 +0100 Subject: [PATCH 4/4] fix(go/plugins/firebase): remove exported uninit --- go/plugins/firebase/firebase.go | 16 ++++------------ go/plugins/firebase/firebase_init_test.go | 12 +++++++++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/go/plugins/firebase/firebase.go b/go/plugins/firebase/firebase.go index c5b2c94921..c04fa2bc2d 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -16,6 +16,7 @@ package firebase import ( "context" + "errors" "fmt" "sync" @@ -60,7 +61,8 @@ func Init(ctx context.Context, cfg *FirebasePluginConfig) error { // Initialize Firebase app with service account key if provided app, err := firebase.NewApp(ctx, firebaseConfig) if err != nil { - return fmt.Errorf("firebase.Init: %w", err) + errorStr := fmt.Sprintf("firebase.Init: %v", err) + return errors.New(errorStr) } state.app = app @@ -69,16 +71,6 @@ func Init(ctx context.Context, cfg *FirebasePluginConfig) error { return nil } -// UnInit uninitializes the Firebase app. - -func UnInit() { - state.mu.Lock() - defer state.mu.Unlock() - - state.initted = false - state.app = nil -} - // App returns a cached Firebase app. // If the app is not initialized, it returns an error. func App(ctx context.Context) (*firebase.App, error) { @@ -86,7 +78,7 @@ func App(ctx context.Context) (*firebase.App, error) { defer state.mu.Unlock() if !state.initted { - return nil, fmt.Errorf("firebase.App: Firebase app not initialized. Call Init first") + return nil, errors.New("firebase.App: Firebase app not initialized. Call Init first") } return state.app, nil diff --git a/go/plugins/firebase/firebase_init_test.go b/go/plugins/firebase/firebase_init_test.go index 7381187f5d..5415cfdd08 100644 --- a/go/plugins/firebase/firebase_init_test.go +++ b/go/plugins/firebase/firebase_init_test.go @@ -20,6 +20,16 @@ import ( "testing" ) +// UnInit uninitializes the Firebase app. + +func uninit() { + state.mu.Lock() + defer state.mu.Unlock() + + state.initted = false + state.app = nil +} + // Define the flag with a default value of "demo-test" var firebaseProjectID = flag.String("firebase-project-id", "demo-test", "Firebase project ID") @@ -67,7 +77,7 @@ func TestInit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defer UnInit() + defer uninit() if err := tt.setup(); err != nil { t.Fatalf("Setup failed: %v", err)