From 8b42b78873ae59a6965495b570ed13a8229f94c5 Mon Sep 17 00:00:00 2001 From: demolaf Date: Tue, 17 Feb 2026 18:06:59 +0100 Subject: [PATCH 01/12] test: firebase app tests and fixes --- .../src/app_check/app_check_http_client.dart | 24 +- .../lib/src/app_check/token_generator.dart | 2 +- .../app/firebase_app_integration_test.dart | 166 +++++++++++ .../test/app/firebase_app_prod_test.dart | 148 ++++++++++ .../test/app/firebase_app_test.dart | 276 ++++++++++++++++++ .../test/app_check/app_check_test.dart | 31 +- 6 files changed, 626 insertions(+), 21 deletions(-) create mode 100644 packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart create mode 100644 packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart diff --git a/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart b/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart index a8ba127a..cfe4af1a 100644 --- a/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart +++ b/packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart @@ -26,7 +26,29 @@ class AppCheckHttpClient { ) async { final client = await app.client; final projectId = await app.getProjectId(); - return fn(client, projectId); + try { + return await fn(client, projectId); + } on FirebaseAppCheckException { + rethrow; + } on appcheck1.DetailedApiRequestError catch (e, stack) { + switch (e.jsonResponse) { + case {'error': {'status': final String status}}: + final code = appCheckErrorCodeMapping[status]; + if (code != null) { + Error.throwWithStackTrace( + FirebaseAppCheckException(code, e.message), + stack, + ); + } + } + Error.throwWithStackTrace( + FirebaseAppCheckException( + AppCheckErrorCode.unknownError, + 'Unexpected error: $e', + ), + stack, + ); + } } /// Executes an App Check v1 API operation with automatic projectId injection. diff --git a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart index ec0dd801..f874b097 100644 --- a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart +++ b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart @@ -33,7 +33,7 @@ class AppCheckTokenGenerator { ]) async { try { final authClient = await app.client; - final account = authClient.getServiceAccountEmail; + final account = await authClient.getServiceAccountEmail; final header = {'alg': 'RS256', 'typ': 'JWT'}; final iat = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); diff --git a/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart new file mode 100644 index 00000000..30435a27 --- /dev/null +++ b/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart @@ -0,0 +1,166 @@ +// Firebase App Integration Tests +// +// Tests real runtime behaviour of FirebaseApp against actual services. +// These tests are separate from the unit tests and require either +// running emulators or real credentials to execute. +// +// Run with Firestore emulator: +// FIRESTORE_EMULATOR_HOST=localhost:8080 \ +// dart test test/app/firebase_app_integration_test.dart +// +// Run with Auth emulator: +// FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \ +// dart test test/app/firebase_app_integration_test.dart +// +// Run the client-creation test (requires GOOGLE_APPLICATION_CREDENTIALS): +// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ +// dart test test/app/firebase_app_integration_test.dart + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_firebase_admin/auth.dart'; +import 'package:dart_firebase_admin/src/app.dart'; +import 'package:dart_firebase_admin/src/auth.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import '../mock_service_account.dart'; + +void main() { + group('FirebaseApp Integration', () { + group( + 'client creation', + () { + tearDown(() { + FirebaseApp.apps.forEach(FirebaseApp.deleteApp); + }); + + test( + 'creates an authenticated client via Application Default Credentials', + () async { + final app = FirebaseApp.initializeApp( + name: 'adc-client-${DateTime.now().microsecondsSinceEpoch}', + options: AppOptions( + credential: Credential.fromApplicationDefaultCredentials(), + projectId: mockProjectId, + ), + ); + + final client = await app.client; + expect(client, isNotNull); + + await app.close(); + }, + ); + }, + skip: !hasGoogleEnv + ? 'Skipping client creation tests. ' + 'Set GOOGLE_APPLICATION_CREDENTIALS to run these tests.' + : false, + ); + + group( + 'Firestore emulator lifecycle', + () { + late FirebaseApp app; + + setUp(() { + app = FirebaseApp.initializeApp( + name: 'fs-lifecycle-${DateTime.now().millisecondsSinceEpoch}', + options: const AppOptions(projectId: projectId), + ); + }); + + tearDown(() async { + if (!app.isDeleted) await app.close(); + }); + + test( + 'initialises Firestore, performs a round-trip, then closes cleanly', + () async { + final firestore = app.firestore(); + final docRef = firestore + .collection('_app_integration') + .doc('lifecycle-ping'); + + await docRef.set({'status': 'alive'}); + + final snap = await docRef.get(); + expect(snap.exists, isTrue); + expect(snap.data()?['status'], 'alive'); + + await docRef.delete(); + + await app.close(); + + expect(app.isDeleted, isTrue); + expect(() => app.firestore(), throwsA(isA())); + }, + timeout: const Timeout(Duration(seconds: 30)), + ); + + test( + 'closes multiple services concurrently without error', + () async { + app.firestore(); + app.messaging(); + app.securityRules(); + + await expectLater(app.close(), completes); + + expect(app.isDeleted, isTrue); + }, + timeout: const Timeout(Duration(seconds: 30)), + ); + }, + skip: Environment.isFirestoreEmulatorEnabled() + ? false + : 'Skipping Firestore emulator lifecycle tests. ' + 'Set FIRESTORE_EMULATOR_HOST to run these tests.', + ); + + group( + 'Auth emulator lifecycle', + () { + // Remove production credentials from the zone so the Auth service + // uses the emulator rather than hitting production. + late Map emulatorEnv; + + setUpAll(() { + emulatorEnv = Map.from(Platform.environment); + emulatorEnv.remove(Environment.googleApplicationCredentials); + }); + + test( + 'initialises Auth, creates a user, then closes cleanly', + () async { + await runZoned(zoneValues: {envSymbol: emulatorEnv}, () async { + final app = FirebaseApp.initializeApp( + name: 'auth-lifecycle-${DateTime.now().millisecondsSinceEpoch}', + options: const AppOptions(projectId: projectId), + ); + + final auth = Auth.internal(app); + + final user = await auth.createUser( + CreateRequest(email: 'lifecycle-test@example.com'), + ); + expect(user.email, 'lifecycle-test@example.com'); + + await auth.deleteUser(user.uid); + + await app.close(); + expect(app.isDeleted, isTrue); + }); + }, + timeout: const Timeout(Duration(seconds: 30)), + ); + }, + skip: Environment.isAuthEmulatorEnabled() + ? false + : 'Skipping Auth emulator lifecycle tests. ' + 'Set FIREBASE_AUTH_EMULATOR_HOST to run these tests.', + ); + }); +} diff --git a/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart new file mode 100644 index 00000000..46af67fd --- /dev/null +++ b/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart @@ -0,0 +1,148 @@ +// Firebase App Production Tests +// +// Covers code paths in FirebaseApp that require real Google credentials: +// - _createDefaultClient() ADC path (lines 122-124) +// - _createDefaultClient() SA path (lines 110-118) +// - close() SDK-client shutdown (line 270) +// - getProjectId() → computeProjectId() (line 152) +// +// Tests are skipped automatically when GOOGLE_APPLICATION_CREDENTIALS is not +// set. They can run alongside emulator tests because each test builds a +// prodEnv zone that strips emulator environment variables. +// +// Run standalone: +// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ +// dart test test/app/firebase_app_prod_test.dart +// +// Run as part of the full suite: +// FIRESTORE_EMULATOR_HOST=localhost:8080 \ +// FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \ +// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ +// dart test + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_firebase_admin/src/app.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + Map prodEnv() { + final env = Map.from(Platform.environment); + env.remove(Environment.firebaseAuthEmulatorHost); + env.remove(Environment.firestoreEmulatorHost); + env.remove(Environment.firebaseStorageEmulatorHost); + env.remove(Environment.cloudTasksEmulatorHost); + return env; + } + + group('FirebaseApp (Production)', () { + group('_createDefaultClient – ADC path', () { + test( + 'creates an authenticated client via Application Default Credentials', + () { + return runZoned(() async { + final app = FirebaseApp.initializeApp( + name: 'adc-client-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(projectId: projectId), + ); + + try { + final client = await app.client; + expect(client, isNotNull); + } finally { + await app.close(); + } + }, zoneValues: {envSymbol: prodEnv()}); + }, + skip: hasGoogleEnv + ? false + : 'Requires GOOGLE_APPLICATION_CREDENTIALS to be set', + timeout: const Timeout(Duration(seconds: 30)), + ); + + test( + 'SDK-created ADC client is closed when app.close() is called', + () { + return runZoned(() async { + final app = FirebaseApp.initializeApp( + name: 'adc-close-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(projectId: projectId), + ); + + await app.client; + await app.close(); + + expect(app.isDeleted, isTrue); + }, zoneValues: {envSymbol: prodEnv()}); + }, + skip: hasGoogleEnv + ? false + : 'Requires GOOGLE_APPLICATION_CREDENTIALS to be set', + timeout: const Timeout(Duration(seconds: 30)), + ); + }); + + group('_createDefaultClient – service account path', () { + test( + 'creates an authenticated client via service account credential', + () { + return runZoned(() async { + final saFile = File( + Platform.environment['GOOGLE_APPLICATION_CREDENTIALS']!, + ); + final credential = Credential.fromServiceAccount(saFile); + + final app = FirebaseApp.initializeApp( + name: 'sa-client-${DateTime.now().microsecondsSinceEpoch}', + options: AppOptions(projectId: projectId, credential: credential), + ); + + try { + final client = await app.client; + expect(client, isNotNull); + } finally { + await app.close(); + } + }, zoneValues: {envSymbol: prodEnv()}); + }, + skip: hasGoogleEnv + ? false + : 'Requires GOOGLE_APPLICATION_CREDENTIALS to be set', + timeout: const Timeout(Duration(seconds: 30)), + ); + }); + + group('getProjectId – computeProjectId fallback', () { + test( + 'falls back to computeProjectId() when no projectId source is configured', + () { + // envSymbol is set to null so Zone.current[envSymbol] == null inside + // getProjectId(), causing env == null and skipping the env-var loop. + // With no projectIdOverride and no options.projectId the method must + // call computeProjectId(), which reads GCP project env vars from + // Platform.environment (e.g. GOOGLE_CLOUD_PROJECT). + return runZoned(() async { + final app = FirebaseApp.initializeApp( + name: 'compute-project-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(), + ); + + try { + final resolved = await app.getProjectId(); + expect(resolved, isNotEmpty); + } finally { + await app.close(); + } + }, zoneValues: {envSymbol: null}); + }, + skip: hasGoogleEnv + ? false + : 'Requires GOOGLE_APPLICATION_CREDENTIALS to be set', + timeout: const Timeout(Duration(seconds: 30)), + ); + }); + }); +} diff --git a/packages/dart_firebase_admin/test/app/firebase_app_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_test.dart index ddb91030..5a4401eb 100644 --- a/packages/dart_firebase_admin/test/app/firebase_app_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_app_test.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'package:dart_firebase_admin/functions.dart'; import 'package:dart_firebase_admin/messaging.dart'; import 'package:dart_firebase_admin/security_rules.dart'; import 'package:dart_firebase_admin/src/app.dart'; import 'package:dart_firebase_admin/src/app_check/app_check.dart'; import 'package:dart_firebase_admin/src/auth.dart'; +import 'package:dart_firebase_admin/storage.dart'; import 'package:googleapis_firestore/googleapis_firestore.dart' as googleapis_firestore; import 'package:mocktail/mocktail.dart'; @@ -223,6 +225,15 @@ void main() { FirebaseApp.deleteApp(appWithoutProject); }); + test('projectId returns the configured value', () { + final app = FirebaseApp.initializeApp( + options: const AppOptions(projectId: mockProjectId), + name: 'configured-project-app', + ); + + expect(app.projectId, mockProjectId); + }); + test('isDeleted returns false for active app', () { final app = FirebaseApp.initializeApp( options: const AppOptions(projectId: mockProjectId), @@ -233,6 +244,170 @@ void main() { }); }); + group('getProjectId', () { + late FirebaseApp app; + + setUp(() { + app = FirebaseApp.initializeApp( + name: 'get-project-id-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(), + ); + }); + + tearDown(() async { + if (!app.isDeleted) await app.close(); + }); + + test( + 'returns project ID from explicit environment map – GOOGLE_CLOUD_PROJECT', + () async { + final resolved = await app.getProjectId( + environment: {'GOOGLE_CLOUD_PROJECT': 'from-google-cloud-project'}, + ); + expect(resolved, 'from-google-cloud-project'); + }, + ); + + test( + 'returns project ID from explicit environment map – GCLOUD_PROJECT', + () async { + final resolved = await app.getProjectId( + environment: {'GCLOUD_PROJECT': 'from-gcloud-project'}, + ); + expect(resolved, 'from-gcloud-project'); + }, + ); + + test( + 'returns project ID from explicit environment map – GCP_PROJECT', + () async { + final resolved = await app.getProjectId( + environment: {'GCP_PROJECT': 'from-gcp-project'}, + ); + expect(resolved, 'from-gcp-project'); + }, + ); + + test( + 'returns project ID from explicit environment map – CLOUDSDK_CORE_PROJECT', + () async { + final resolved = await app.getProjectId( + environment: { + 'CLOUDSDK_CORE_PROJECT': 'from-cloudsdk-core-project', + }, + ); + expect(resolved, 'from-cloudsdk-core-project'); + }, + ); + + test('returns project ID from zone-injected environment', () async { + await runZoned( + zoneValues: { + envSymbol: {'GOOGLE_CLOUD_PROJECT': 'zone-project'}, + }, + () async { + final resolved = await app.getProjectId(); + expect(resolved, 'zone-project'); + }, + ); + }); + + test( + 'explicit environment map takes precedence over projectIdOverride', + () async { + final resolved = await app.getProjectId( + projectIdOverride: 'override-project', + environment: {'GOOGLE_CLOUD_PROJECT': 'env-wins'}, + ); + expect(resolved, 'env-wins'); + }, + ); + + test( + 'zone environment takes precedence over projectIdOverride', + () async { + await runZoned( + zoneValues: { + envSymbol: {'GOOGLE_CLOUD_PROJECT': 'zone-wins'}, + }, + () async { + final resolved = await app.getProjectId( + projectIdOverride: 'override-loses', + ); + expect(resolved, 'zone-wins'); + }, + ); + }, + ); + + test( + 'explicit environment map takes precedence over options.projectId', + () async { + final appWithProject = FirebaseApp.initializeApp( + name: 'env-over-options-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(projectId: 'options-project'), + ); + addTearDown(() async { + if (!appWithProject.isDeleted) await appWithProject.close(); + }); + + final resolved = await appWithProject.getProjectId( + environment: {'GOOGLE_CLOUD_PROJECT': 'env-wins-over-options'}, + ); + expect(resolved, 'env-wins-over-options'); + }, + ); + + test( + 'projectIdOverride takes precedence over options.projectId', + () async { + final appWithProject = FirebaseApp.initializeApp( + name: + 'override-over-options-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(projectId: 'options-project'), + ); + addTearDown(() async { + if (!appWithProject.isDeleted) await appWithProject.close(); + }); + + final resolved = await appWithProject.getProjectId( + projectIdOverride: 'override-wins', + environment: {}, + ); + expect(resolved, 'override-wins'); + }, + ); + + test( + 'returns projectIdOverride when no environment variables are set', + () async { + final resolved = await app.getProjectId( + projectIdOverride: 'only-override', + environment: {}, + ); + expect(resolved, 'only-override'); + }, + ); + + test( + 'returns options.projectId when no env vars and no override', + () async { + final appWithProject = FirebaseApp.initializeApp( + name: 'options-fallback-${DateTime.now().microsecondsSinceEpoch}', + options: const AppOptions(projectId: 'configured-project'), + ); + addTearDown(() async { + if (!appWithProject.isDeleted) await appWithProject.close(); + }); + + final resolved = await appWithProject.getProjectId( + environment: {}, + ); + expect(resolved, 'configured-project'); + }, + ); + }); + group('client', () { test('returns custom client when provided', () async { final mockClient = MockAuthClient(); @@ -409,6 +584,32 @@ void main() { expect(identical(securityRules1, securityRules2), isTrue); }); + test('functions returns Functions instance', () { + final functions = app.functions(); + expect(functions, isA()); + expect(identical(functions.app, app), isTrue); + }); + + test('functions returns cached instance', () { + final functions1 = app.functions(); + final functions2 = app.functions(); + expect(identical(functions1, functions2), isTrue); + expect(identical(functions1, Functions.internal(app)), isTrue); + }); + + test('storage returns Storage instance', () { + final storage = app.storage(); + expect(storage, isA()); + expect(identical(storage.app, app), isTrue); + }); + + test('storage returns cached instance', () { + final storage1 = app.storage(); + final storage2 = app.storage(); + expect(identical(storage1, storage2), isTrue); + expect(identical(storage1, Storage.internal(app)), isTrue); + }); + test('throws when accessing services after deletion', () async { await app.close(); @@ -433,6 +634,81 @@ void main() { ), ); }); + + test('appCheck throws when accessing after deletion', () async { + await app.close(); + + expect( + () => app.appCheck(), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'app/app-deleted', + ), + ), + ); + }); + + test('messaging throws when accessing after deletion', () async { + await app.close(); + + expect( + () => app.messaging(), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'app/app-deleted', + ), + ), + ); + }); + + test('securityRules throws when accessing after deletion', () async { + await app.close(); + + expect( + () => app.securityRules(), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'app/app-deleted', + ), + ), + ); + }); + + test('functions throws when accessing after deletion', () async { + await app.close(); + + expect( + () => app.functions(), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'app/app-deleted', + ), + ), + ); + }); + + test('storage throws when accessing after deletion', () async { + await app.close(); + + expect( + () => app.storage(), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'app/app-deleted', + ), + ), + ); + }); }); group('close', () { diff --git a/packages/dart_firebase_admin/test/app_check/app_check_test.dart b/packages/dart_firebase_admin/test/app_check/app_check_test.dart index 98f0166e..114bacb1 100644 --- a/packages/dart_firebase_admin/test/app_check/app_check_test.dart +++ b/packages/dart_firebase_admin/test/app_check/app_check_test.dart @@ -327,15 +327,19 @@ void main() { }); group('e2e', () { + Map prodEnv() { + final env = Map.from(Platform.environment); + env.remove(Environment.firebaseAuthEmulatorHost); + env.remove(Environment.firestoreEmulatorHost); + env.remove(Environment.firebaseStorageEmulatorHost); + env.remove(Environment.cloudTasksEmulatorHost); + return env; + } + test( skip: hasGoogleEnv ? false : 'Requires GOOGLE_APPLICATION_CREDENTIALS', 'should create and verify token', () { - // Remove emulator env var from the zone environment - final prodEnv = Map.from(Platform.environment); - // App Check doesn't have emulator yet, but keep pattern consistent - // prodEnv.remove(Environment.appCheckEmulatorHost); - return runZoned(() async { final appName = 'prod-test-${DateTime.now().microsecondsSinceEpoch}'; @@ -358,7 +362,7 @@ void main() { } finally { await app.close(); } - }, zoneValues: {envSymbol: prodEnv}); + }, zoneValues: {envSymbol: prodEnv()}); }, ); @@ -366,11 +370,6 @@ void main() { skip: hasGoogleEnv ? false : 'Requires GOOGLE_APPLICATION_CREDENTIALS', 'should create token with custom ttl', () { - // Remove emulator env var from the zone environment - final prodEnv = Map.from(Platform.environment); - // App Check doesn't have emulator yet, but keep pattern consistent - // prodEnv.remove(Environment.appCheckEmulatorHost); - return runZoned(() async { final appName = 'prod-test-${DateTime.now().microsecondsSinceEpoch}'; @@ -384,12 +383,11 @@ void main() { ); expect(token.token, isNotEmpty); - // TTL might not be exactly what we requested, but should be reasonable expect(token.ttlMillis, greaterThan(0)); } finally { await app.close(); } - }, zoneValues: {envSymbol: prodEnv}); + }, zoneValues: {envSymbol: prodEnv()}); }, ); @@ -397,11 +395,6 @@ void main() { skip: hasGoogleEnv ? false : 'Requires GOOGLE_APPLICATION_CREDENTIALS', 'should verify token with consume option', () { - // Remove emulator env var from the zone environment - final prodEnv = Map.from(Platform.environment); - // App Check doesn't have emulator yet, but keep pattern consistent - // prodEnv.remove(Environment.appCheckEmulatorHost); - return runZoned(() async { final appName = 'prod-test-${DateTime.now().microsecondsSinceEpoch}'; @@ -432,7 +425,7 @@ void main() { } finally { await app.close(); } - }, zoneValues: {envSymbol: prodEnv}); + }, zoneValues: {envSymbol: prodEnv()}); }, ); }); From b217a1781897e08f1b998eb1d6fb4ffe25dbdb9a Mon Sep 17 00:00:00 2001 From: demolaf Date: Tue, 17 Feb 2026 18:45:17 +0100 Subject: [PATCH 02/12] chore: code cleanup --- .../app/firebase_app_integration_test.dart | 18 ------------- .../test/app/firebase_app_prod_test.dart | 27 ------------------- .../test/app/firebase_app_test.dart | 23 ---------------- 3 files changed, 68 deletions(-) diff --git a/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart index 30435a27..22ac96f9 100644 --- a/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_app_integration_test.dart @@ -1,21 +1,3 @@ -// Firebase App Integration Tests -// -// Tests real runtime behaviour of FirebaseApp against actual services. -// These tests are separate from the unit tests and require either -// running emulators or real credentials to execute. -// -// Run with Firestore emulator: -// FIRESTORE_EMULATOR_HOST=localhost:8080 \ -// dart test test/app/firebase_app_integration_test.dart -// -// Run with Auth emulator: -// FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \ -// dart test test/app/firebase_app_integration_test.dart -// -// Run the client-creation test (requires GOOGLE_APPLICATION_CREDENTIALS): -// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ -// dart test test/app/firebase_app_integration_test.dart - import 'dart:async'; import 'dart:io'; diff --git a/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart index 46af67fd..a6360d99 100644 --- a/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_app_prod_test.dart @@ -1,25 +1,3 @@ -// Firebase App Production Tests -// -// Covers code paths in FirebaseApp that require real Google credentials: -// - _createDefaultClient() ADC path (lines 122-124) -// - _createDefaultClient() SA path (lines 110-118) -// - close() SDK-client shutdown (line 270) -// - getProjectId() → computeProjectId() (line 152) -// -// Tests are skipped automatically when GOOGLE_APPLICATION_CREDENTIALS is not -// set. They can run alongside emulator tests because each test builds a -// prodEnv zone that strips emulator environment variables. -// -// Run standalone: -// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ -// dart test test/app/firebase_app_prod_test.dart -// -// Run as part of the full suite: -// FIRESTORE_EMULATOR_HOST=localhost:8080 \ -// FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \ -// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \ -// dart test - import 'dart:async'; import 'dart:io'; @@ -119,11 +97,6 @@ void main() { test( 'falls back to computeProjectId() when no projectId source is configured', () { - // envSymbol is set to null so Zone.current[envSymbol] == null inside - // getProjectId(), causing env == null and skipping the env-var loop. - // With no projectIdOverride and no options.projectId the method must - // call computeProjectId(), which reads GCP project env vars from - // Platform.environment (e.g. GOOGLE_CLOUD_PROJECT). return runZoned(() async { final app = FirebaseApp.initializeApp( name: 'compute-project-${DateTime.now().microsecondsSinceEpoch}', diff --git a/packages/dart_firebase_admin/test/app/firebase_app_test.dart b/packages/dart_firebase_admin/test/app/firebase_app_test.dart index 5a4401eb..2eac615a 100644 --- a/packages/dart_firebase_admin/test/app/firebase_app_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_app_test.dart @@ -16,8 +16,6 @@ import '../helpers.dart'; import '../mock.dart'; import '../mock_service_account.dart'; -// TODO(demolaf): check if we have sufficient tests for firebase app initialization -// logic void main() { group('FirebaseApp', () { group('initializeApp', () { @@ -421,27 +419,6 @@ void main() { await FirebaseApp.deleteApp(app); }); - // TODO(demolaf): this test would need to be an e2e test. - // test('creates authenticated client when service account provided', - // () async { - // final credential = Credential.fromServiceAccountParams( - // privateKey: mockPrivateKey, - // email: mockClientEmail, - // projectId: mockProjectId, - // ); - // final app = FirebaseApp.initializeApp( - // options: AppOptions( - // projectId: mockProjectId, - // credential: credential, - // ), - // ); - // - // final client = await app.client; - // expect(client, isA()); - // - // await FirebaseApp.deleteApp(app); - // }); - test('reuses same client on subsequent calls', () { runZoned(() async { final mockClient = MockAuthClient(); From 1782bcd2fbf562338f80dfce891a697043e5585d Mon Sep 17 00:00:00 2001 From: demolaf Date: Tue, 17 Feb 2026 20:18:44 +0100 Subject: [PATCH 03/12] feat: add FirebaseUserAgentClient to track SDK usage --- .../lib/dart_firebase_admin.dart | 1 + packages/dart_firebase_admin/lib/src/app.dart | 2 + .../lib/src/app/firebase_app.dart | 23 ++- .../src/app/firebase_user_agent_client.dart | 29 ++++ .../src/messaging/messaging_http_client.dart | 6 +- .../app/firebase_user_agent_client_test.dart | 135 ++++++++++++++++++ 6 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart create mode 100644 packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart diff --git a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart index 56e3a5cb..fe7bfa47 100644 --- a/packages/dart_firebase_admin/lib/dart_firebase_admin.dart +++ b/packages/dart_firebase_admin/lib/dart_firebase_admin.dart @@ -6,6 +6,7 @@ export 'src/app.dart' AppRegistry, EmulatorClient, Environment, + FirebaseUserAgentClient, FirebaseServiceType, FirebaseService, CloudTasksEmulatorClient; diff --git a/packages/dart_firebase_admin/lib/src/app.dart b/packages/dart_firebase_admin/lib/src/app.dart index 51f6413e..47882137 100644 --- a/packages/dart_firebase_admin/lib/src/app.dart +++ b/packages/dart_firebase_admin/lib/src/app.dart @@ -21,6 +21,7 @@ import '../functions.dart'; import '../messaging.dart'; import '../security_rules.dart'; import '../storage.dart'; +import '../version.g.dart'; part 'app/app_exception.dart'; part 'app/app_options.dart'; @@ -30,4 +31,5 @@ part 'app/emulator_client.dart'; part 'app/environment.dart'; part 'app/exception.dart'; part 'app/firebase_app.dart'; +part 'app/firebase_user_agent_client.dart'; part 'app/firebase_service.dart'; diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart index 48ad70a6..b3ac558e 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart @@ -107,21 +107,18 @@ class FirebaseApp { final credential = options.credential; // Create authenticated client based on credential type - if (credential != null) { - final serviceAccountCreds = credential.serviceAccountCredentials; - if (serviceAccountCreds != null) { - // Use service account credentials - return googleapis_auth.clientViaServiceAccount( - serviceAccountCreds, + final client = switch (credential) { + Credential(:final serviceAccountCredentials?) => + googleapis_auth.clientViaServiceAccount( + serviceAccountCredentials, scopes, - ); - } - } + ), + _ => googleapis_auth.clientViaApplicationDefaultCredentials( + scopes: scopes, + ), + }; - // Fall back to Application Default Credentials - return googleapis_auth.clientViaApplicationDefaultCredentials( - scopes: scopes, - ); + return FirebaseUserAgentClient(await client); } /// Returns the HTTP client for this app. diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart new file mode 100644 index 00000000..518f883c --- /dev/null +++ b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart @@ -0,0 +1,29 @@ +part of '../app.dart'; + +/// HTTP client wrapper that adds the `X-Firebase-Client` header for usage tracking. +/// +/// Wraps another HTTP client and injects `X-Firebase-Client: fire-admin-dart/{version}` +/// into every outgoing request so Firebase backend services can identify the SDK. +@internal +class FirebaseUserAgentClient extends BaseClient + implements googleapis_auth.AuthClient { + FirebaseUserAgentClient(this._client); + + final googleapis_auth.AuthClient _client; + + @override + googleapis_auth.AccessCredentials get credentials => _client.credentials; + + @override + googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => + _client.serviceAccountCredentials; + + @override + Future send(BaseRequest request) { + request.headers['X-Firebase-Client'] = 'fire-admin-dart/$packageVersion'; + return _client.send(request); + } + + @override + void close() => _client.close(); +} diff --git a/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart b/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart index 9f06c4d6..bf745b3b 100644 --- a/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart +++ b/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart @@ -1,10 +1,6 @@ part of 'messaging.dart'; -final _legacyFirebaseMessagingHeaders = { - // TODO send version - 'X-Firebase-Client': 'fire-admin-node/12.0.0', - 'access_token_auth': 'true', -}; +final _legacyFirebaseMessagingHeaders = {'access_token_auth': 'true'}; /// HTTP client for Firebase Cloud Messaging API operations. /// diff --git a/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart new file mode 100644 index 00000000..3b274a39 --- /dev/null +++ b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:dart_firebase_admin/src/app.dart'; +import 'package:dart_firebase_admin/version.g.dart'; +import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; +import 'package:http/http.dart'; +import 'package:test/test.dart'; + +void main() { + group('FirebaseUserAgentClient', () { + test('adds X-Firebase-Client header to every request', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + await client.send(Request('GET', Uri.parse('https://example.com/'))); + + expect(captured.length, 1); + expect( + captured.first.headers['X-Firebase-Client'], + 'fire-admin-dart/$packageVersion', + ); + }); + + test('header value is fire-admin-dart/', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + await client.send(Request('GET', Uri.parse('https://example.com/'))); + + final value = captured.first.headers['X-Firebase-Client']!; + expect(value, startsWith('fire-admin-dart/')); + expect(value.split('/').last, packageVersion); + }); + + test('preserves other headers on the request', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + final request = Request('POST', Uri.parse('https://example.com/')); + request.headers['content-type'] = 'application/json'; + request.headers['Authorization'] = 'Bearer tok'; + await client.send(request); + + expect(captured.first.headers['content-type'], 'application/json'); + expect(captured.first.headers['Authorization'], 'Bearer tok'); + }); + + test('overwrites any pre-existing X-Firebase-Client header', () async { + // The legacy messaging client used to set fire-admin-node/; + // FirebaseUserAgentClient should replace it with the correct value. + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + final request = Request('POST', Uri.parse('https://example.com/')); + request.headers['X-Firebase-Client'] = 'fire-admin-node/12.0.0'; + await client.send(request); + + expect( + captured.first.headers['X-Firebase-Client'], + 'fire-admin-dart/$packageVersion', + ); + }); + + test('injects header on every individual request', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + await client.send(Request('GET', Uri.parse('https://example.com/1'))); + await client.send(Request('POST', Uri.parse('https://example.com/2'))); + await client.send(Request('PUT', Uri.parse('https://example.com/3'))); + + expect(captured.length, 3); + for (final req in captured) { + expect( + req.headers['X-Firebase-Client'], + 'fire-admin-dart/$packageVersion', + ); + } + }); + + test('delegates close() to the inner client', () async { + var closed = false; + final client = FirebaseUserAgentClient( + _CapturingAuthClient([], onClose: () => closed = true), + ); + + client.close(); + + expect(closed, isTrue); + }); + + test('delegates credentials getter to the inner client', () { + final inner = _CapturingAuthClient([]); + final client = FirebaseUserAgentClient(inner); + + // credentials throws UnimplementedError on our stub — same as EmulatorClient. + expect(() => client.credentials, throwsUnimplementedError); + }); + + test('delegates serviceAccountCredentials getter to the inner client', () { + final inner = _CapturingAuthClient([]); + final client = FirebaseUserAgentClient(inner); + + expect(client.serviceAccountCredentials, isNull); + }); + }); +} + +/// Minimal [googleapis_auth.AuthClient] that records every [BaseRequest] +/// passed to [send] without making real network calls. +class _CapturingAuthClient extends BaseClient + implements googleapis_auth.AuthClient { + _CapturingAuthClient(this._captured, {void Function()? onClose}) + : _onClose = onClose; + + final List _captured; + final void Function()? _onClose; + + @override + googleapis_auth.AccessCredentials get credentials => + throw UnimplementedError(); + + @override + googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => + null; + + @override + Future send(BaseRequest request) async { + _captured.add(request); + return StreamedResponse(const Stream.empty(), 200); + } + + @override + void close() => _onClose?.call(); +} From ff5a7c530b10deb36c5bd1eca1bc7d9d9434c4d3 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 17 Feb 2026 16:27:09 -0800 Subject: [PATCH 04/12] Update packages for googleapis_auth breaking changes Accommodate breaking changes from googleapis_auth, specifically the removal of the `serviceAccountCredentials` getter and the refactoring of cryptographic signing. - Update `AuthClient.sign()` usages across dart_firebase_admin and googleapis_storage. The method now returns a `Future` (base64-encoded signature) instead of an object with a `signedBlob` property. - Explicitly pass `serviceAccountCredentials` into `sign()` invocations where available to maintain local RSA signing capabilities. - Remove `serviceAccountCredentials` overrides from EmulatorClient implementations in dart_firebase_admin, googleapis_storage, and googleapis_firestore. - Update Compute Engine environment detection in FunctionsRequestHandler to check internal credential options instead of the removed AuthClient.serviceAccountCredentials. - Delete local AuthExtension helpers in dart_firebase_admin and googleapis_storage, migrating to the newly provided AuthClientSigningExtension methods. - Update authClient.getServiceAccountEmail usages from a getter to a method invocation. --- packages/dart_firebase_admin/lib/src/app.dart | 1 + .../lib/src/app/emulator_client.dart | 8 -------- .../lib/src/app/firebase_app.dart | 2 +- .../lib/src/app_check/token_generator.dart | 9 ++++++--- packages/dart_firebase_admin/lib/src/auth.dart | 1 - .../lib/src/auth/token_generator.dart | 8 ++++++-- .../lib/src/functions/functions.dart | 1 - .../src/functions/functions_request_handler.dart | 10 ++++------ .../lib/src/utils/auth_extension.dart | 16 ---------------- .../lib/src/firestore_http_client.dart | 7 ++----- .../lib/googleapis_storage.dart | 1 - packages/googleapis_storage/lib/src/file.dart | 2 +- .../lib/src/internal/emulator_client.dart | 4 ---- packages/googleapis_storage/lib/src/signer.dart | 4 ++-- .../lib/src/utils/auth_extension.dart | 7 ------- pubspec.yaml | 4 ++++ 16 files changed, 27 insertions(+), 58 deletions(-) delete mode 100644 packages/dart_firebase_admin/lib/src/utils/auth_extension.dart delete mode 100644 packages/googleapis_storage/lib/src/utils/auth_extension.dart diff --git a/packages/dart_firebase_admin/lib/src/app.dart b/packages/dart_firebase_admin/lib/src/app.dart index 51f6413e..ea6792c5 100644 --- a/packages/dart_firebase_admin/lib/src/app.dart +++ b/packages/dart_firebase_admin/lib/src/app.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:equatable/equatable.dart'; +import 'package:google_cloud/constants.dart' as google_cloud; import 'package:google_cloud/google_cloud.dart' as google_cloud; import 'package:googleapis/identitytoolkit/v3.dart' as auth3; import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; diff --git a/packages/dart_firebase_admin/lib/src/app/emulator_client.dart b/packages/dart_firebase_admin/lib/src/app/emulator_client.dart index e77822f3..ddf9ff21 100644 --- a/packages/dart_firebase_admin/lib/src/app/emulator_client.dart +++ b/packages/dart_firebase_admin/lib/src/app/emulator_client.dart @@ -35,10 +35,6 @@ class EmulatorClient extends BaseClient implements googleapis_auth.AuthClient { googleapis_auth.AccessCredentials get credentials => throw UnimplementedError(); - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - null; - @override Future send(BaseRequest request) async { final modifiedRequest = _RequestImpl( @@ -76,10 +72,6 @@ class CloudTasksEmulatorClient implements googleapis_auth.AuthClient { googleapis_auth.AccessCredentials get credentials => throw UnimplementedError(); - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - null; - /// Rewrites the URL to remove `/v2/` prefix and route to emulator host. Uri _rewriteUrl(Uri url) { // Replace the path: remove /v2/ prefix if present diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart index 48ad70a6..41c77c89 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_app.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_app.dart @@ -140,7 +140,7 @@ class FirebaseApp { }) async { final env = environment ?? Zone.current[envSymbol] as Map?; if (env != null) { - for (final envKey in google_cloud.gcpProjectIdEnvironmentVariables) { + for (final envKey in google_cloud.projectIdEnvironmentVariableOptions) { final value = env[envKey]; if (value != null) return value; } diff --git a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart index ec0dd801..2e1e03d3 100644 --- a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart +++ b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart @@ -4,7 +4,6 @@ import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; import 'package:meta/meta.dart'; import '../../dart_firebase_admin.dart'; -import '../utils/auth_extension.dart'; import 'app_check.dart'; import 'app_check_api.dart'; @@ -33,7 +32,7 @@ class AppCheckTokenGenerator { ]) async { try { final authClient = await app.client; - final account = authClient.getServiceAccountEmail; + final account = await authClient.getServiceAccountEmail(); final header = {'alg': 'RS256', 'typ': 'JWT'}; final iat = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); @@ -48,7 +47,11 @@ class AppCheckTokenGenerator { final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; - final signature = await authClient.signBlob(utf8.encode(token)); + final signature = await authClient.sign( + utf8.encode(token), + serviceAccountCredentials: + app.options.credential?.serviceAccountCredentials, + ); return '$token.$signature'; } on googleapis_auth.ServerRequestFailedException catch (err) { diff --git a/packages/dart_firebase_admin/lib/src/auth.dart b/packages/dart_firebase_admin/lib/src/auth.dart index d7eccdf2..83502ca4 100644 --- a/packages/dart_firebase_admin/lib/src/auth.dart +++ b/packages/dart_firebase_admin/lib/src/auth.dart @@ -14,7 +14,6 @@ import 'package:meta/meta.dart'; import 'app.dart'; import 'object_utils.dart'; -import 'utils/auth_extension.dart'; import 'utils/jwt.dart'; import 'utils/utils.dart'; import 'utils/validator.dart'; diff --git a/packages/dart_firebase_admin/lib/src/auth/token_generator.dart b/packages/dart_firebase_admin/lib/src/auth/token_generator.dart index 054f551b..eeedadab 100644 --- a/packages/dart_firebase_admin/lib/src/auth/token_generator.dart +++ b/packages/dart_firebase_admin/lib/src/auth/token_generator.dart @@ -71,7 +71,7 @@ class _FirebaseTokenGenerator { try { final authClient = await _app.client; - final account = await authClient.getServiceAccountEmail; + final account = await authClient.getServiceAccountEmail(); final header = {'alg': 'RS256', 'typ': 'JWT'}; final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; @@ -87,7 +87,11 @@ class _FirebaseTokenGenerator { }; final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; - final signature = await authClient.signBlob(utf8.encode(token)); + final signature = await authClient.sign( + utf8.encode(token), + serviceAccountCredentials: + _app.options.credential?.serviceAccountCredentials, + ); return '$token.$signature'; } on googleapis_auth.ServerRequestFailedException catch (err, stack) { diff --git a/packages/dart_firebase_admin/lib/src/functions/functions.dart b/packages/dart_firebase_admin/lib/src/functions/functions.dart index fe338fed..1a5463f0 100644 --- a/packages/dart_firebase_admin/lib/src/functions/functions.dart +++ b/packages/dart_firebase_admin/lib/src/functions/functions.dart @@ -5,7 +5,6 @@ import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; import 'package:meta/meta.dart'; import '../app.dart'; -import '../utils/auth_extension.dart'; import '../utils/validator.dart'; part 'functions_api.dart'; diff --git a/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart b/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart index 4f86ea2d..1e97bf5e 100644 --- a/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart +++ b/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart @@ -253,11 +253,9 @@ class FunctionsRequestHandler { return; } - // Check if running as an extension with ComputeEngine credentials. - // ComputeEngine credentials are used when running on GCE/Cloud Run without - // a service account JSON file - indicated by credentials without local - // service account credentials (i.e., using metadata server). - final isComputeEngine = authClient.serviceAccountCredentials == null; + // Service credentials via `FirebaseApp.options`. + final isComputeEngine = + _httpClient.app.options.credential?.serviceAccountCredentials == null; if (extensionId != null && extensionId.isNotEmpty && isComputeEngine) { // Running as extension with ComputeEngine - use ID token with Authorization header. @@ -276,7 +274,7 @@ class FunctionsRequestHandler { // Default: Use OIDC token with service account email. // Try to get service account email from credential first, then from metadata service. - final serviceAccountEmail = await authClient.getServiceAccountEmail; + final serviceAccountEmail = await authClient.getServiceAccountEmail(); if (serviceAccountEmail.isEmpty) { throw FirebaseFunctionsAdminException( diff --git a/packages/dart_firebase_admin/lib/src/utils/auth_extension.dart b/packages/dart_firebase_admin/lib/src/utils/auth_extension.dart deleted file mode 100644 index c99bee95..00000000 --- a/packages/dart_firebase_admin/lib/src/utils/auth_extension.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; - -import '../app.dart'; - -extension AuthExtension on googleapis_auth.AuthClient { - Future get getServiceAccountEmail async => - serviceAccountCredentials?.email ?? - googleapis_auth.IAMSigner(this).getServiceAccountEmail(); - - /// Signs the given data using the IAM Credentials API or local credentials. - /// - /// Returns a base64-encoded signature string. In emulator mode, returns an - /// empty string to produce unsigned tokens. - Future signBlob(List data, {String? endpoint}) async => - Environment.isAuthEmulatorEnabled() ? '' : sign(data, endpoint: endpoint); -} diff --git a/packages/googleapis_firestore/lib/src/firestore_http_client.dart b/packages/googleapis_firestore/lib/src/firestore_http_client.dart index 8e99a919..f0201873 100644 --- a/packages/googleapis_firestore/lib/src/firestore_http_client.dart +++ b/packages/googleapis_firestore/lib/src/firestore_http_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:google_cloud/constants.dart' as google_cloud; import 'package:google_cloud/google_cloud.dart' as google_cloud; import 'package:googleapis/firestore/v1.dart' as firestore_v1; import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; @@ -45,10 +46,6 @@ class EmulatorClient extends BaseClient implements googleapis_auth.AuthClient { googleapis_auth.AccessCredentials get credentials => throw UnimplementedError(); - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - null; - @override Future send(BaseRequest request) async { final modifiedRequest = _RequestImpl( @@ -130,7 +127,7 @@ class FirestoreHttpClient { final env = _settings.environmentOverride; if (env != null) { - for (final envKey in google_cloud.gcpProjectIdEnvironmentVariables) { + for (final envKey in google_cloud.projectIdEnvironmentVariableOptions) { final value = env[envKey]; if (value != null) { projectId = value; diff --git a/packages/googleapis_storage/lib/googleapis_storage.dart b/packages/googleapis_storage/lib/googleapis_storage.dart index 42f94642..be023667 100644 --- a/packages/googleapis_storage/lib/googleapis_storage.dart +++ b/packages/googleapis_storage/lib/googleapis_storage.dart @@ -12,7 +12,6 @@ import 'package:googleapis_auth/auth_io.dart'; import 'package:googleapis_storage/src/internal/api_error.dart'; import 'package:googleapis_storage/src/internal/api.dart'; import 'package:googleapis_storage/src/internal/service.dart'; -import 'package:googleapis_storage/src/utils/auth_extension.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; diff --git a/packages/googleapis_storage/lib/src/file.dart b/packages/googleapis_storage/lib/src/file.dart index 89437c4a..34542512 100644 --- a/packages/googleapis_storage/lib/src/file.dart +++ b/packages/googleapis_storage/lib/src/file.dart @@ -1056,7 +1056,7 @@ class BucketFile extends ServiceObject // Get auth client and credentials final authClient = await storage.authClient; - final clientEmail = await authClient.getServiceAccountEmail; + final clientEmail = await authClient.getServiceAccountEmail(); // Build credential string final todayISO = _formatDateStamp(now); diff --git a/packages/googleapis_storage/lib/src/internal/emulator_client.dart b/packages/googleapis_storage/lib/src/internal/emulator_client.dart index bc41827d..7eedb3b8 100644 --- a/packages/googleapis_storage/lib/src/internal/emulator_client.dart +++ b/packages/googleapis_storage/lib/src/internal/emulator_client.dart @@ -37,10 +37,6 @@ class EmulatorClient extends BaseClient implements googleapis_auth.AuthClient { googleapis_auth.AccessCredentials get credentials => throw UnimplementedError('EmulatorClient does not provide credentials'); - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - null; - @override Future send(BaseRequest request) async { final modifiedRequest = _RequestImpl( diff --git a/packages/googleapis_storage/lib/src/signer.dart b/packages/googleapis_storage/lib/src/signer.dart index 2ea4f9b3..9090bedb 100644 --- a/packages/googleapis_storage/lib/src/signer.dart +++ b/packages/googleapis_storage/lib/src/signer.dart @@ -138,7 +138,7 @@ class URLSigner { endpoint: config.signedConfig.signingEndpoint?.toString(), ); - final clientEmail = await authClient.getServiceAccountEmail; + final clientEmail = await authClient.getServiceAccountEmail(); return { 'GoogleAccessId': clientEmail, @@ -202,7 +202,7 @@ class URLSigner { final authClient = await bucket.storage.authClient; - final clientEmail = await authClient.getServiceAccountEmail; + final clientEmail = await authClient.getServiceAccountEmail(); final credentialString = '$clientEmail/$credentialScope'; final dateISO = _formatAsUTCISO(config.accessibleAt, includeTime: true); diff --git a/packages/googleapis_storage/lib/src/utils/auth_extension.dart b/packages/googleapis_storage/lib/src/utils/auth_extension.dart deleted file mode 100644 index f7adb620..00000000 --- a/packages/googleapis_storage/lib/src/utils/auth_extension.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; - -extension AuthExtension on googleapis_auth.AuthClient { - Future get getServiceAccountEmail async => - serviceAccountCredentials?.email ?? - googleapis_auth.IAMSigner(this).getServiceAccountEmail(); -} diff --git a/pubspec.yaml b/pubspec.yaml index ccb41a6a..be1fc9ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,10 @@ dev_dependencies: melos: ^7.3.0 test: ^1.26.3 +dependency_overrides: + googleapis_auth: + path: ../googleapis.dart/googleapis_auth + workspace: - packages/dart_firebase_admin - packages/googleapis_firestore From aa349ce1365dcfff2cfb130d7c581941993c4f3d Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 17 Feb 2026 16:39:04 -0800 Subject: [PATCH 05/12] OVERRIDES for in-flight PR Will replace when https://github.com/google/googleapis.dart/pull/717 lands --- packages/dart_firebase_admin/pubspec.yaml | 2 +- packages/googleapis_firestore/pubspec.yaml | 2 +- packages/googleapis_storage/pubspec.yaml | 2 +- pubspec.yaml | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/dart_firebase_admin/pubspec.yaml b/packages/dart_firebase_admin/pubspec.yaml index 5cccb66d..2a3985a2 100644 --- a/packages/dart_firebase_admin/pubspec.yaml +++ b/packages/dart_firebase_admin/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: collection: ^1.18.0 dart_jsonwebtoken: ^3.0.0 equatable: ^2.0.7 - google_cloud: ^0.2.1-beta.1 + google_cloud: ^0.3.0 googleapis: ^15.0.0 googleapis_auth: ^2.1.0-beta.1 googleapis_beta: ^9.0.0 diff --git a/packages/googleapis_firestore/pubspec.yaml b/packages/googleapis_firestore/pubspec.yaml index 519e4fb4..ffc9a077 100644 --- a/packages/googleapis_firestore/pubspec.yaml +++ b/packages/googleapis_firestore/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: collection: ^1.18.0 - google_cloud: ^0.2.1-beta.1 + google_cloud: ^0.3.0 googleapis: ^15.0.0 googleapis_auth: ^2.0.0 http: ^1.0.0 diff --git a/packages/googleapis_storage/pubspec.yaml b/packages/googleapis_storage/pubspec.yaml index 8d84b3d8..623738fc 100644 --- a/packages/googleapis_storage/pubspec.yaml +++ b/packages/googleapis_storage/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: googleapis_auth: ^2.0.0 googleapis: ^15.0.0 - google_cloud: ^0.2.0 + google_cloud: ^0.3.0 http: ^1.6.0 meta: ^1.17.0 mime: ^2.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index be1fc9ce..6595653b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,10 @@ dev_dependencies: dependency_overrides: googleapis_auth: - path: ../googleapis.dart/googleapis_auth + git: + url: https://github.com/google/googleapis.dart.git + ref: drop_signer_class + path: googleapis_auth workspace: - packages/dart_firebase_admin From 9b2e07f1b3ed0548c2a1d2ee664c5f6d9bd9526e Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 17 Feb 2026 16:48:25 -0800 Subject: [PATCH 06/12] test: fix functions test mock for metadata server email resolution --- .../test/functions/functions_test.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/dart_firebase_admin/test/functions/functions_test.dart b/packages/dart_firebase_admin/test/functions/functions_test.dart index 2282d83d..0aea0059 100644 --- a/packages/dart_firebase_admin/test/functions/functions_test.dart +++ b/packages/dart_firebase_admin/test/functions/functions_test.dart @@ -26,6 +26,7 @@ class FakeBaseRequest extends Fake implements BaseRequest {} /// Creates a mock HTTP client that handles OAuth token requests and /// optionally Cloud Tasks API requests. MockClient createMockHttpClient({ + String? email, String? idToken, Response Function(Request)? apiHandler, }) { @@ -60,6 +61,15 @@ MockClient createMockHttpClient({ ); } + // Handle Metadata Server requests for service account email + if (request.url.host == 'metadata.google.internal' && + request.url.path.contains('/service-accounts/default/email')) { + if (email != null) { + return Response(email, 200, headers: {'Metadata-Flavor': 'Google'}); + } + return Response('Not Found', 404); + } + // Default response return Response('{}', 200); }); @@ -75,6 +85,7 @@ Future createTestAuthClient({ Response Function(Request)? apiHandler, }) async { final baseClient = createMockHttpClient( + email: email, idToken: idToken, apiHandler: apiHandler, ); From 859fefdf9f9c89c25d79fbe36b36816ac68e3f9b Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 18 Feb 2026 18:13:33 +0100 Subject: [PATCH 07/12] feat: add extension methods for FirebaseApp to handle service account email and signing --- .../lib/src/app/app_registry.dart | 19 +++++++++++---- .../lib/src/app_check/token_generator.dart | 10 +++----- .../dart_firebase_admin/lib/src/auth.dart | 1 + .../lib/src/auth/token_generator.dart | 9 ++------ .../lib/src/functions/functions.dart | 1 + .../functions/functions_request_handler.dart | 2 +- .../lib/src/utils/app_extension.dart | 23 +++++++++++++++++++ .../test/auth/auth_test.dart | 9 +++++++- .../dart_firebase_admin/test/helpers.dart | 7 +++++- packages/googleapis_storage/lib/src/file.dart | 8 ++++++- .../googleapis_storage/lib/src/signer.dart | 12 ++++++++-- 11 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 packages/dart_firebase_admin/lib/src/utils/app_extension.dart diff --git a/packages/dart_firebase_admin/lib/src/app/app_registry.dart b/packages/dart_firebase_admin/lib/src/app/app_registry.dart index c89e7253..3db9cd43 100644 --- a/packages/dart_firebase_admin/lib/src/app/app_registry.dart +++ b/packages/dart_firebase_admin/lib/src/app/app_registry.dart @@ -100,9 +100,7 @@ class AppRegistry { final config = env['FIREBASE_CONFIG']; if (config == null || config.isEmpty) { - return AppOptions( - credential: Credential.fromApplicationDefaultCredentials(), - ); + return AppOptions(credential: _credentialFromEnv(env)); } try { @@ -118,7 +116,7 @@ class AppRegistry { final json = jsonDecode(contents) as Map; return AppOptions( - credential: Credential.fromApplicationDefaultCredentials(), + credential: _credentialFromEnv(env), projectId: json['projectId'] as String?, databaseURL: json['databaseURL'] as String?, storageBucket: json['storageBucket'] as String?, @@ -186,4 +184,17 @@ class AppRegistry { ); } } + + Credential _credentialFromEnv(Map env) { + final googleCredPath = env['GOOGLE_APPLICATION_CREDENTIALS']; + if (googleCredPath != null) { + try { + return Credential.fromServiceAccount(File(googleCredPath)); + } catch (_) { + // File missing, not a service account JSON, or missing project_id – + // fall through to ADC. + } + } + return Credential.fromApplicationDefaultCredentials(); + } } diff --git a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart index 2e1e03d3..dcb2906d 100644 --- a/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart +++ b/packages/dart_firebase_admin/lib/src/app_check/token_generator.dart @@ -4,6 +4,7 @@ import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; import 'package:meta/meta.dart'; import '../../dart_firebase_admin.dart'; +import '../utils/app_extension.dart'; import 'app_check.dart'; import 'app_check_api.dart'; @@ -31,8 +32,7 @@ class AppCheckTokenGenerator { AppCheckTokenOptions? options, ]) async { try { - final authClient = await app.client; - final account = await authClient.getServiceAccountEmail(); + final account = await app.serviceAccountEmail; final header = {'alg': 'RS256', 'typ': 'JWT'}; final iat = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); @@ -47,11 +47,7 @@ class AppCheckTokenGenerator { final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; - final signature = await authClient.sign( - utf8.encode(token), - serviceAccountCredentials: - app.options.credential?.serviceAccountCredentials, - ); + final signature = await app.sign(utf8.encode(token)); return '$token.$signature'; } on googleapis_auth.ServerRequestFailedException catch (err) { diff --git a/packages/dart_firebase_admin/lib/src/auth.dart b/packages/dart_firebase_admin/lib/src/auth.dart index 83502ca4..5b25abcb 100644 --- a/packages/dart_firebase_admin/lib/src/auth.dart +++ b/packages/dart_firebase_admin/lib/src/auth.dart @@ -14,6 +14,7 @@ import 'package:meta/meta.dart'; import 'app.dart'; import 'object_utils.dart'; +import 'utils/app_extension.dart'; import 'utils/jwt.dart'; import 'utils/utils.dart'; import 'utils/validator.dart'; diff --git a/packages/dart_firebase_admin/lib/src/auth/token_generator.dart b/packages/dart_firebase_admin/lib/src/auth/token_generator.dart index eeedadab..620a8699 100644 --- a/packages/dart_firebase_admin/lib/src/auth/token_generator.dart +++ b/packages/dart_firebase_admin/lib/src/auth/token_generator.dart @@ -70,8 +70,7 @@ class _FirebaseTokenGenerator { } try { - final authClient = await _app.client; - final account = await authClient.getServiceAccountEmail(); + final account = await _app.serviceAccountEmail; final header = {'alg': 'RS256', 'typ': 'JWT'}; final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; @@ -87,11 +86,7 @@ class _FirebaseTokenGenerator { }; final token = '${_encodeSegment(header)}.${_encodeSegment(body)}'; - final signature = await authClient.sign( - utf8.encode(token), - serviceAccountCredentials: - _app.options.credential?.serviceAccountCredentials, - ); + final signature = await _app.sign(utf8.encode(token)); return '$token.$signature'; } on googleapis_auth.ServerRequestFailedException catch (err, stack) { diff --git a/packages/dart_firebase_admin/lib/src/functions/functions.dart b/packages/dart_firebase_admin/lib/src/functions/functions.dart index 1a5463f0..0e0c6f40 100644 --- a/packages/dart_firebase_admin/lib/src/functions/functions.dart +++ b/packages/dart_firebase_admin/lib/src/functions/functions.dart @@ -5,6 +5,7 @@ import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; import 'package:meta/meta.dart'; import '../app.dart'; +import '../utils/app_extension.dart'; import '../utils/validator.dart'; part 'functions_api.dart'; diff --git a/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart b/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart index 1e97bf5e..d5e669a7 100644 --- a/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart +++ b/packages/dart_firebase_admin/lib/src/functions/functions_request_handler.dart @@ -274,7 +274,7 @@ class FunctionsRequestHandler { // Default: Use OIDC token with service account email. // Try to get service account email from credential first, then from metadata service. - final serviceAccountEmail = await authClient.getServiceAccountEmail(); + final serviceAccountEmail = await _httpClient.app.serviceAccountEmail; if (serviceAccountEmail.isEmpty) { throw FirebaseFunctionsAdminException( diff --git a/packages/dart_firebase_admin/lib/src/utils/app_extension.dart b/packages/dart_firebase_admin/lib/src/utils/app_extension.dart new file mode 100644 index 00000000..05b5da06 --- /dev/null +++ b/packages/dart_firebase_admin/lib/src/utils/app_extension.dart @@ -0,0 +1,23 @@ +import 'package:googleapis_auth/auth_io.dart'; + +import '../app.dart'; + +extension AppExtension on FirebaseApp { + Future get serviceAccountEmail async => + options.credential?.serviceAccountCredentials?.email ?? + (await client).getServiceAccountEmail(); + + /// Signs the given data using the IAM Credentials API or local credentials. + /// + /// Returns a base64-encoded signature string. In emulator mode, returns an + /// empty string to produce unsigned tokens. + Future sign(List data, {String? endpoint}) async => + Environment.isAuthEmulatorEnabled() + ? '' + : (await client).sign( + data, + serviceAccountCredentials: + options.credential?.serviceAccountCredentials, + endpoint: endpoint, + ); +} diff --git a/packages/dart_firebase_admin/test/auth/auth_test.dart b/packages/dart_firebase_admin/test/auth/auth_test.dart index c77da1d6..bccb4f7c 100644 --- a/packages/dart_firebase_admin/test/auth/auth_test.dart +++ b/packages/dart_firebase_admin/test/auth/auth_test.dart @@ -9,12 +9,19 @@ import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import '../helpers.dart'; import '../mock.dart'; +import '../mock_service_account.dart'; void main() { late Auth auth; setUp(() { - final sdk = createApp(); + final sdk = createApp( + credential: Credential.fromServiceAccountParams( + privateKey: mockPrivateKey, + email: mockClientEmail, + projectId: projectId, + ), + ); auth = Auth.internal(sdk); }); diff --git a/packages/dart_firebase_admin/test/helpers.dart b/packages/dart_firebase_admin/test/helpers.dart index da58f7a8..aea1b6d5 100644 --- a/packages/dart_firebase_admin/test/helpers.dart +++ b/packages/dart_firebase_admin/test/helpers.dart @@ -39,10 +39,15 @@ FirebaseApp createApp({ FutureOr Function()? tearDown, googleapis_auth.AuthClient? client, String? name, + Credential? credential, }) { final app = FirebaseApp.initializeApp( name: name, - options: AppOptions(projectId: projectId, httpClient: client), + options: AppOptions( + projectId: projectId, + httpClient: client, + credential: credential, + ), ); addTearDown(() async { diff --git a/packages/googleapis_storage/lib/src/file.dart b/packages/googleapis_storage/lib/src/file.dart index 34542512..f5d3b8e9 100644 --- a/packages/googleapis_storage/lib/src/file.dart +++ b/packages/googleapis_storage/lib/src/file.dart @@ -1003,6 +1003,8 @@ class BucketFile extends ServiceObject final authClient = await storage.authClient; final signature = await authClient.sign( policyStringBytes, + serviceAccountCredentials: + bucket.storage.options.credential?.serviceAccountCredentials, endpoint: options.signingEndpoint?.toString(), ); @@ -1056,7 +1058,9 @@ class BucketFile extends ServiceObject // Get auth client and credentials final authClient = await storage.authClient; - final clientEmail = await authClient.getServiceAccountEmail(); + final clientEmail = + bucket.storage.options.credential?.serviceAccountCredentials?.email ?? + await authClient.getServiceAccountEmail(); // Build credential string final todayISO = _formatDateStamp(now); @@ -1098,6 +1102,8 @@ class BucketFile extends ServiceObject try { final signature = await authClient.sign( policyStringBytes, + serviceAccountCredentials: + bucket.storage.options.credential?.serviceAccountCredentials, endpoint: options.signingEndpoint?.toString(), ); diff --git a/packages/googleapis_storage/lib/src/signer.dart b/packages/googleapis_storage/lib/src/signer.dart index 9090bedb..b48a376b 100644 --- a/packages/googleapis_storage/lib/src/signer.dart +++ b/packages/googleapis_storage/lib/src/signer.dart @@ -135,10 +135,14 @@ class URLSigner { final authClient = await bucket.storage.authClient; final signature = await authClient.sign( utf8.encode(blobToSign), + serviceAccountCredentials: + bucket.storage.options.credential?.serviceAccountCredentials, endpoint: config.signedConfig.signingEndpoint?.toString(), ); - final clientEmail = await authClient.getServiceAccountEmail(); + final clientEmail = + bucket.storage.options.credential?.serviceAccountCredentials?.email ?? + await authClient.getServiceAccountEmail(); return { 'GoogleAccessId': clientEmail, @@ -202,7 +206,9 @@ class URLSigner { final authClient = await bucket.storage.authClient; - final clientEmail = await authClient.getServiceAccountEmail(); + final clientEmail = + bucket.storage.options.credential?.serviceAccountCredentials?.email ?? + await authClient.getServiceAccountEmail(); final credentialString = '$clientEmail/$credentialScope'; final dateISO = _formatAsUTCISO(config.accessibleAt, includeTime: true); @@ -239,6 +245,8 @@ class URLSigner { final signature = await authClient.sign( utf8.encode(blobToSign), + serviceAccountCredentials: + bucket.storage.options.credential?.serviceAccountCredentials, endpoint: config.signedConfig.signingEndpoint?.toString(), ); From 514a79781bb4cc74304115b8bdbdd4896b8765fe Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 18 Feb 2026 19:49:42 +0100 Subject: [PATCH 08/12] chore: bump dart_firebase_admin to 0.5.0 and googleapis_auth dependency to 2.1.0 --- packages/dart_firebase_admin/lib/version.g.dart | 2 +- packages/dart_firebase_admin/pubspec.yaml | 4 ++-- packages/googleapis_firestore/pubspec.yaml | 2 +- packages/googleapis_storage/pubspec.yaml | 2 +- pubspec.yaml | 7 ------- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/dart_firebase_admin/lib/version.g.dart b/packages/dart_firebase_admin/lib/version.g.dart index f70f7cfb..59c5d2ac 100644 --- a/packages/dart_firebase_admin/lib/version.g.dart +++ b/packages/dart_firebase_admin/lib/version.g.dart @@ -2,4 +2,4 @@ // This file is generated by gen-version.sh /// The current version of the package. -const String packageVersion = '1.0.0-beta.1'; +const String packageVersion = '0.5.0'; diff --git a/packages/dart_firebase_admin/pubspec.yaml b/packages/dart_firebase_admin/pubspec.yaml index 2a3985a2..db1a9e94 100644 --- a/packages/dart_firebase_admin/pubspec.yaml +++ b/packages/dart_firebase_admin/pubspec.yaml @@ -1,7 +1,7 @@ name: dart_firebase_admin description: A Firebase Admin SDK implementation for Dart. resolution: workspace -version: 1.0.0-beta.1 +version: 0.5.0 homepage: "https://github.com/invertase/dart_firebase_admin" repository: "https://github.com/invertase/dart_firebase_admin" publish_to: none @@ -16,7 +16,7 @@ dependencies: equatable: ^2.0.7 google_cloud: ^0.3.0 googleapis: ^15.0.0 - googleapis_auth: ^2.1.0-beta.1 + googleapis_auth: ^2.1.0 googleapis_beta: ^9.0.0 googleapis_firestore: ^0.1.0 googleapis_storage: ^0.1.0 diff --git a/packages/googleapis_firestore/pubspec.yaml b/packages/googleapis_firestore/pubspec.yaml index ffc9a077..eb1e03de 100644 --- a/packages/googleapis_firestore/pubspec.yaml +++ b/packages/googleapis_firestore/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: collection: ^1.18.0 google_cloud: ^0.3.0 googleapis: ^15.0.0 - googleapis_auth: ^2.0.0 + googleapis_auth: ^2.1.0 http: ^1.0.0 intl: ^0.20.0 meta: ^1.9.1 diff --git a/packages/googleapis_storage/pubspec.yaml b/packages/googleapis_storage/pubspec.yaml index 623738fc..3459e0d6 100644 --- a/packages/googleapis_storage/pubspec.yaml +++ b/packages/googleapis_storage/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=3.9.0 <4.0.0" dependencies: - googleapis_auth: ^2.0.0 + googleapis_auth: ^2.1.0 googleapis: ^15.0.0 google_cloud: ^0.3.0 http: ^1.6.0 diff --git a/pubspec.yaml b/pubspec.yaml index 6595653b..ccb41a6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,13 +8,6 @@ dev_dependencies: melos: ^7.3.0 test: ^1.26.3 -dependency_overrides: - googleapis_auth: - git: - url: https://github.com/google/googleapis.dart.git - ref: drop_signer_class - path: googleapis_auth - workspace: - packages/dart_firebase_admin - packages/googleapis_firestore From e36f0db2f91e2e10ea6c77f8b8f66f26df21f269 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 18 Feb 2026 19:56:39 +0100 Subject: [PATCH 09/12] chore: update changelog --- packages/dart_firebase_admin/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart_firebase_admin/CHANGELOG.md b/packages/dart_firebase_admin/CHANGELOG.md index 22838e09..58875912 100644 --- a/packages/dart_firebase_admin/CHANGELOG.md +++ b/packages/dart_firebase_admin/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.0.0-beta.1 - 2026-02-12 +## 0.5.0 - 2026-02-12 - Major changes From 8f7ec4ecc505cdab566c95ea1972bb347433859d Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 18 Feb 2026 20:01:08 +0100 Subject: [PATCH 10/12] chore: code cleanup --- .../lib/src/messaging/messaging_http_client.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart b/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart index bf745b3b..0ce7d45d 100644 --- a/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart +++ b/packages/dart_firebase_admin/lib/src/messaging/messaging_http_client.dart @@ -1,7 +1,5 @@ part of 'messaging.dart'; -final _legacyFirebaseMessagingHeaders = {'access_token_auth': 'true'}; - /// HTTP client for Firebase Cloud Messaging API operations. /// /// Handles HTTP client management, googleapis API client creation, @@ -53,7 +51,7 @@ class FirebaseMessagingHttpClient { Uri.https(host, path), body: jsonEncode(requestData), headers: { - ..._legacyFirebaseMessagingHeaders, + 'access_token_auth': 'true', 'content-type': 'application/json', }, ); From ce3a13c2bfe08d51a483e02af30b5ecfcf1f503f Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 18 Feb 2026 20:12:44 +0100 Subject: [PATCH 11/12] fix ci --- .../lib/src/app/firebase_user_agent_client.dart | 4 ---- .../test/app/firebase_user_agent_client_test.dart | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart index 518f883c..d4b48d69 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart @@ -14,10 +14,6 @@ class FirebaseUserAgentClient extends BaseClient @override googleapis_auth.AccessCredentials get credentials => _client.credentials; - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - _client.serviceAccountCredentials; - @override Future send(BaseRequest request) { request.headers['X-Firebase-Client'] = 'fire-admin-dart/$packageVersion'; diff --git a/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart index 3b274a39..7d9d4bb9 100644 --- a/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart @@ -96,13 +96,6 @@ void main() { // credentials throws UnimplementedError on our stub — same as EmulatorClient. expect(() => client.credentials, throwsUnimplementedError); }); - - test('delegates serviceAccountCredentials getter to the inner client', () { - final inner = _CapturingAuthClient([]); - final client = FirebaseUserAgentClient(inner); - - expect(client.serviceAccountCredentials, isNull); - }); }); } @@ -120,10 +113,6 @@ class _CapturingAuthClient extends BaseClient googleapis_auth.AccessCredentials get credentials => throw UnimplementedError(); - @override - googleapis_auth.ServiceAccountCredentials? get serviceAccountCredentials => - null; - @override Future send(BaseRequest request) async { _captured.add(request); From 5312a95a3e9706268b0ae9946f3e8cafe3461780 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 19 Feb 2026 15:40:23 +0100 Subject: [PATCH 12/12] feat: add X-Goog-Api-Client header --- packages/dart_firebase_admin/lib/src/app.dart | 1 + .../src/app/firebase_user_agent_client.dart | 8 ++- .../lib/src/utils/utils.dart | 6 +++ .../app/firebase_user_agent_client_test.dart | 51 ++++++++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/app.dart b/packages/dart_firebase_admin/lib/src/app.dart index 03a0fb95..e47b016e 100644 --- a/packages/dart_firebase_admin/lib/src/app.dart +++ b/packages/dart_firebase_admin/lib/src/app.dart @@ -23,6 +23,7 @@ import '../messaging.dart'; import '../security_rules.dart'; import '../storage.dart'; import '../version.g.dart'; +import 'utils/utils.dart'; part 'app/app_exception.dart'; part 'app/app_options.dart'; diff --git a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart index d4b48d69..45c9e47a 100644 --- a/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart +++ b/packages/dart_firebase_admin/lib/src/app/firebase_user_agent_client.dart @@ -1,8 +1,10 @@ part of '../app.dart'; -/// HTTP client wrapper that adds the `X-Firebase-Client` header for usage tracking. +/// HTTP client wrapper that adds Firebase and Google API client headers for usage tracking. /// -/// Wraps another HTTP client and injects `X-Firebase-Client: fire-admin-dart/{version}` +/// Wraps another HTTP client and injects: +/// - `X-Firebase-Client: fire-admin-dart/{version}` +/// - `X-Goog-Api-Client: gl-dart/{dartVersion} fire-admin-dart/{version}` /// into every outgoing request so Firebase backend services can identify the SDK. @internal class FirebaseUserAgentClient extends BaseClient @@ -17,6 +19,8 @@ class FirebaseUserAgentClient extends BaseClient @override Future send(BaseRequest request) { request.headers['X-Firebase-Client'] = 'fire-admin-dart/$packageVersion'; + request.headers['X-Goog-Api-Client'] = + 'gl-dart/$dartVersion fire-admin-dart/$packageVersion'; return _client.send(request); } diff --git a/packages/dart_firebase_admin/lib/src/utils/utils.dart b/packages/dart_firebase_admin/lib/src/utils/utils.dart index e7ca8677..ca9d37ec 100644 --- a/packages/dart_firebase_admin/lib/src/utils/utils.dart +++ b/packages/dart_firebase_admin/lib/src/utils/utils.dart @@ -1,3 +1,9 @@ +import 'dart:io'; + +/// The current Dart SDK version in semver format (e.g. "3.3.0"). +String get dartVersion => + Platform.version.split(RegExp('[^0-9]')).take(3).join('.'); + /// Generates the update mask for the provided object. /// Note this will ignore the last key with value undefined. List generateUpdateMask( diff --git a/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart index 7d9d4bb9..4f1ae441 100644 --- a/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart +++ b/packages/dart_firebase_admin/test/app/firebase_user_agent_client_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:dart_firebase_admin/src/app.dart'; +import 'package:dart_firebase_admin/src/utils/utils.dart'; import 'package:dart_firebase_admin/version.g.dart'; import 'package:googleapis_auth/auth_io.dart' as googleapis_auth; import 'package:http/http.dart'; @@ -32,6 +33,36 @@ void main() { expect(value.split('/').last, packageVersion); }); + test('adds X-Goog-Api-Client header to every request', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + await client.send(Request('GET', Uri.parse('https://example.com/'))); + + expect(captured.length, 1); + expect( + captured.first.headers['X-Goog-Api-Client'], + 'gl-dart/$dartVersion fire-admin-dart/$packageVersion', + ); + }); + + test( + 'X-Goog-Api-Client has correct format gl-dart/ fire-admin-dart/', + () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + await client.send(Request('GET', Uri.parse('https://example.com/'))); + + final value = captured.first.headers['X-Goog-Api-Client']!; + final parts = value.split(' '); + expect(parts.length, 2); + expect(parts[0], startsWith('gl-dart/')); + expect(parts[1], startsWith('fire-admin-dart/')); + expect(parts[1].split('/').last, packageVersion); + }, + ); + test('preserves other headers on the request', () async { final captured = []; final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); @@ -61,7 +92,21 @@ void main() { ); }); - test('injects header on every individual request', () async { + test('overwrites any pre-existing X-Goog-Api-Client header', () async { + final captured = []; + final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); + + final request = Request('POST', Uri.parse('https://example.com/')); + request.headers['X-Goog-Api-Client'] = 'gl-node/18.0.0 fire-admin/12.0.0'; + await client.send(request); + + expect( + captured.first.headers['X-Goog-Api-Client'], + 'gl-dart/$dartVersion fire-admin-dart/$packageVersion', + ); + }); + + test('injects both headers on every individual request', () async { final captured = []; final client = FirebaseUserAgentClient(_CapturingAuthClient(captured)); @@ -75,6 +120,10 @@ void main() { req.headers['X-Firebase-Client'], 'fire-admin-dart/$packageVersion', ); + expect( + req.headers['X-Goog-Api-Client'], + 'gl-dart/$dartVersion fire-admin-dart/$packageVersion', + ); } });