diff --git a/go/plugins/firebase/auth_test.go b/go/plugins/firebase/auth_test.go index 6bd566064..3af9a79f8 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 e2f4a85b4..c04fa2bc2 100644 --- a/go/plugins/firebase/firebase.go +++ b/go/plugins/firebase/firebase.go @@ -16,31 +16,70 @@ package firebase import ( "context" + "errors" + "fmt" "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 { + // throw an error if the app is already initialized + return errors.New("firebase.Init: Firebase app already initialized") + } + + // 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 { + errorStr := fmt.Sprintf("firebase.Init: %v", err) + return errors.New(errorStr) } - 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, errors.New("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 000000000..19528c49e --- /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 000000000..5415cfdd0 --- /dev/null +++ b/go/plugins/firebase/firebase_init_test.go @@ -0,0 +1,97 @@ +// 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" +) + +// 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") + +func TestInit(t *testing.T) { + + 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: "firebase.Init: Firebase app already initialized", + 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) { + defer uninit() + + 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) + } + }) + } +}