Skip to content

Commit 368fc67

Browse files
authored
refactor: HTTP clients and add AppRegistry tests (#108)
* refactor(dart): apply DRY pattern to HTTP clients for consistent client/projectId handling Add `_run` helper method to MessagingHttpClient, SecurityRulesHttpClient, and AppCheckHttpClient that accepts both client and projectId as callback parameters. This eliminates redundant `await app.client` calls and centralizes client/projectId retrieval logic. * refactor: update exception constructors to use FirebaseServiceType for consistency * test: add AppRegistry tests for singleton behavior and environment options * refactor: update pubspec.yaml for workspace resolution and dependency management
1 parent 55bc07a commit 368fc67

17 files changed

+5436
-141
lines changed

packages/dart_firebase_admin/coverage.lcov

Lines changed: 4811 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
name: dart_firebase_admin_example
22
publish_to: none
3+
resolution: workspace
34

45
environment:
56
sdk: '>=3.9.0 <4.0.0'
67

78
dependencies:
8-
dart_firebase_admin:
9-
path: ../
10-
11-
dependency_overrides:
12-
googleapis_auth_utils:
13-
path: ../../googleapis_auth_utils
9+
dart_firebase_admin: any

packages/dart_firebase_admin/lib/src/app_check/app_check.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:googleapis/firebaseappcheck/v1.dart' as appcheck1;
4+
import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
45
import 'package:googleapis_auth_utils/googleapis_auth_utils.dart';
56
import 'package:googleapis_beta/firebaseappcheck/v1beta.dart' as appcheck1_beta;
67
import 'package:meta/meta.dart';

packages/dart_firebase_admin/lib/src/app_check/app_check_exception.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ enum AppCheckErrorCode {
4444
/// [message] - The error message.
4545
class FirebaseAppCheckException extends FirebaseAdminException {
4646
FirebaseAppCheckException(AppCheckErrorCode code, [String? _message])
47-
: super('app-check', code.code, _message);
47+
: super(FirebaseServiceType.appCheck.name, code.code, _message);
4848

4949
factory FirebaseAppCheckException.fromJwtException(JwtException error) {
5050
if (error.code == JwtErrorCode.tokenExpired) {

packages/dart_firebase_admin/lib/src/app_check/app_check_http_client.dart

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,36 @@ class AppCheckHttpClient {
2020
return 'projects/$projectId';
2121
}
2222

23-
/// Executes an App Check v1 API operation with automatic projectId injection.
24-
Future<R> v1<R>(
25-
Future<R> Function(appcheck1.FirebaseappcheckApi client, String projectId)
26-
fn,
23+
Future<R> _run<R>(
24+
Future<R> Function(googleapis_auth.AuthClient client, String projectId) fn,
2725
) async {
2826
final client = await app.client;
2927
final projectId = await client.getProjectId(
3028
projectIdOverride: app.options.projectId,
3129
environment: Zone.current[envSymbol] as Map<String, String>?,
3230
);
33-
return fn(appcheck1.FirebaseappcheckApi(client), projectId);
31+
return fn(client, projectId);
3432
}
3533

34+
/// Executes an App Check v1 API operation with automatic projectId injection.
35+
Future<R> v1<R>(
36+
Future<R> Function(appcheck1.FirebaseappcheckApi client, String projectId)
37+
fn,
38+
) => _run(
39+
(client, projectId) => fn(appcheck1.FirebaseappcheckApi(client), projectId),
40+
);
41+
3642
/// Executes an App Check v1Beta API operation with automatic projectId injection.
3743
Future<R> v1Beta<R>(
3844
Future<R> Function(
3945
appcheck1_beta.FirebaseappcheckApi client,
4046
String projectId,
4147
)
4248
fn,
43-
) async {
44-
final client = await app.client;
45-
final projectId = await client.getProjectId(
46-
projectIdOverride: app.options.projectId,
47-
environment: Zone.current[envSymbol] as Map<String, String>?,
48-
);
49-
return fn(appcheck1_beta.FirebaseappcheckApi(client), projectId);
50-
}
49+
) => _run(
50+
(client, projectId) =>
51+
fn(appcheck1_beta.FirebaseappcheckApi(client), projectId),
52+
);
5153

5254
/// Exchange a custom token for an App Check token (low-level API call).
5355
///

packages/dart_firebase_admin/lib/src/auth/auth_exception.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ part of '../auth.dart';
33
class FirebaseAuthAdminException extends FirebaseAdminException
44
implements Exception {
55
FirebaseAuthAdminException(this.errorCode, [String? message])
6-
: super('auth', errorCode.code, message ?? errorCode.message);
6+
: super(
7+
FirebaseServiceType.auth.name,
8+
errorCode.code,
9+
message ?? errorCode.message,
10+
);
711

812
factory FirebaseAuthAdminException.fromServerError({
913
required String serverErrorCode,

packages/dart_firebase_admin/lib/src/auth/auth_http_client.dart

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ class AuthHttpClient {
4444
return app.client;
4545
}
4646

47-
// TODO handle tenants
48-
4947
/// Builds the parent resource path for project-level operations.
5048
String buildParent(String projectId) {
5149
return 'projects/$projectId';
@@ -61,6 +59,11 @@ class AuthHttpClient {
6159
return 'projects/$projectId/inboundSamlConfigs/$parentId';
6260
}
6361

62+
/// Builds the resource path for a specific tenant.
63+
String buildTenantParent(String projectId, String tenantId) {
64+
return 'projects/$projectId/tenants/$tenantId';
65+
}
66+
6467
Future<auth1.GoogleCloudIdentitytoolkitV1GetOobCodeResponse> getOobCode(
6568
auth1.GoogleCloudIdentitytoolkitV1GetOobCodeRequest request,
6669
) {
@@ -305,65 +308,7 @@ class AuthHttpClient {
305308
});
306309
}
307310

308-
Future<R> _run<R>(Future<R> Function(googleapis_auth.AuthClient client) fn) {
309-
return _authGuard(() async {
310-
// Use the cached client (created once based on emulator configuration)
311-
final client = await _client;
312-
return fn(client);
313-
});
314-
}
315-
316-
Future<R> v1<R>(
317-
Future<R> Function(auth1.IdentityToolkitApi client, String projectId) fn,
318-
) async {
319-
// TODO(demolaf): this can move into _run instead
320-
final client = await this.client;
321-
final projectId = await client.getProjectId(
322-
projectIdOverride: app.options.projectId,
323-
environment: Zone.current[envSymbol] as Map<String, String>?,
324-
);
325-
return _run(
326-
(client) => fn(
327-
auth1.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
328-
projectId,
329-
),
330-
);
331-
}
332-
333-
Future<R> v2<R>(
334-
Future<R> Function(auth2.IdentityToolkitApi client, String projectId) fn,
335-
) async {
336-
final client = await this.client;
337-
final projectId = await client.getProjectId(
338-
projectIdOverride: app.options.projectId,
339-
environment: Zone.current[envSymbol] as Map<String, String>?,
340-
);
341-
return _run(
342-
(client) => fn(
343-
auth2.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
344-
projectId,
345-
),
346-
);
347-
}
348-
349-
Future<R> v3<R>(
350-
Future<R> Function(auth3.IdentityToolkitApi client, String projectId) fn,
351-
) async {
352-
final client = await this.client;
353-
final projectId = await client.getProjectId(
354-
projectIdOverride: app.options.projectId,
355-
environment: Zone.current[envSymbol] as Map<String, String>?,
356-
);
357-
return _run(
358-
(client) => fn(
359-
auth3.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
360-
projectId,
361-
),
362-
);
363-
}
364-
365311
// Tenant management methods
366-
367312
Future<auth2.GoogleCloudIdentitytoolkitAdminV2Tenant> getTenant(
368313
String tenantId,
369314
) {
@@ -376,7 +321,7 @@ class AuthHttpClient {
376321
}
377322

378323
final response = await client.projects.tenants.get(
379-
'projects/$projectId/tenants/$tenantId',
324+
buildTenantParent(projectId, tenantId),
380325
);
381326

382327
if (response.name == null || response.name!.isEmpty) {
@@ -394,7 +339,7 @@ class AuthHttpClient {
394339
listTenants({required int maxResults, String? pageToken}) {
395340
return v2((client, projectId) async {
396341
final response = await client.projects.tenants.list(
397-
'projects/$projectId',
342+
buildParent(projectId),
398343
pageSize: maxResults,
399344
pageToken: pageToken,
400345
);
@@ -413,7 +358,7 @@ class AuthHttpClient {
413358
}
414359

415360
return client.projects.tenants.delete(
416-
'projects/$projectId/tenants/$tenantId',
361+
buildTenantParent(projectId, tenantId),
417362
);
418363
});
419364
}
@@ -424,7 +369,7 @@ class AuthHttpClient {
424369
return v2((client, projectId) async {
425370
final response = await client.projects.tenants.create(
426371
request,
427-
'projects/$projectId',
372+
buildParent(projectId),
428373
);
429374

430375
if (response.name == null || response.name!.isEmpty) {
@@ -450,7 +395,7 @@ class AuthHttpClient {
450395
);
451396
}
452397

453-
final name = 'projects/$projectId/tenants/$tenantId';
398+
final name = buildTenantParent(projectId, tenantId);
454399
final updateMask = request.toJson().keys.join(',');
455400

456401
final response = await client.projects.tenants.patch(
@@ -469,6 +414,47 @@ class AuthHttpClient {
469414
return response;
470415
});
471416
}
417+
418+
Future<R> _run<R>(
419+
Future<R> Function(googleapis_auth.AuthClient client, String projectId) fn,
420+
) {
421+
return _authGuard(() async {
422+
// Use the cached client (created once based on emulator configuration)
423+
final client = await _client;
424+
final projectId = await client.getProjectId(
425+
projectIdOverride: app.options.projectId,
426+
environment: Zone.current[envSymbol] as Map<String, String>?,
427+
);
428+
return fn(client, projectId);
429+
});
430+
}
431+
432+
Future<R> v1<R>(
433+
Future<R> Function(auth1.IdentityToolkitApi client, String projectId) fn,
434+
) => _run(
435+
(client, projectId) => fn(
436+
auth1.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
437+
projectId,
438+
),
439+
);
440+
441+
Future<R> v2<R>(
442+
Future<R> Function(auth2.IdentityToolkitApi client, String projectId) fn,
443+
) => _run(
444+
(client, projectId) => fn(
445+
auth2.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
446+
projectId,
447+
),
448+
);
449+
450+
Future<R> v3<R>(
451+
Future<R> Function(auth3.IdentityToolkitApi client, String projectId) fn,
452+
) => _run(
453+
(client, projectId) => fn(
454+
auth3.IdentityToolkitApi(client, rootUrl: _authApiHost.toString()),
455+
projectId,
456+
),
457+
);
472458
}
473459

474460
/// Tenant-aware HTTP client that builds tenant-specific resource paths.

packages/dart_firebase_admin/lib/src/google_cloud_firestore/firestore_exception.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ Never _handleException(Object exception, StackTrace stackTrace) {
119119
class FirebaseFirestoreAdminException extends FirebaseAdminException
120120
implements Exception {
121121
FirebaseFirestoreAdminException(this.errorCode, [String? message])
122-
: super('firestore', errorCode.code, message ?? errorCode.message);
122+
: super(
123+
FirebaseServiceType.firestore.name,
124+
errorCode.code,
125+
message ?? errorCode.message,
126+
);
123127

124128
@internal
125129
factory FirebaseFirestoreAdminException.fromServerError({

packages/dart_firebase_admin/lib/src/google_cloud_firestore/firestore_http_client.dart

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,16 @@ class FirestoreHttpClient {
5050
}
5151

5252
Future<R> _run<R>(
53-
Future<R> Function(googleapis_auth.AuthClient client) fn,
53+
Future<R> Function(googleapis_auth.AuthClient client, String projectId) fn,
5454
) async {
5555
// Use the cached client (created once based on emulator configuration)
5656
final client = await _client;
57-
return _firestoreGuard(() => fn(client));
57+
final projectId = await client.getProjectId(
58+
projectIdOverride: app.options.projectId,
59+
environment: Zone.current[envSymbol] as Map<String, String>?,
60+
);
61+
_cachedProjectId = projectId;
62+
return _firestoreGuard(() => fn(client, projectId));
5863
}
5964

6065
/// Executes a Firestore v1 API operation with automatic projectId injection.
@@ -63,18 +68,10 @@ class FirestoreHttpClient {
6368
/// all subsequent operations. This matches the Auth service pattern.
6469
Future<R> v1<R>(
6570
Future<R> Function(firestore1.FirestoreApi client, String projectId) fn,
66-
) async {
67-
final client = await _client;
68-
final projectId = await client.getProjectId(
69-
projectIdOverride: app.options.projectId,
70-
environment: Zone.current[envSymbol] as Map<String, String>?,
71-
);
72-
_cachedProjectId = projectId;
73-
return _run(
74-
(client) => fn(
75-
firestore1.FirestoreApi(client, rootUrl: _firestoreApiHost.toString()),
76-
projectId,
77-
),
78-
);
79-
}
71+
) => _run(
72+
(client, projectId) => fn(
73+
firestore1.FirestoreApi(client, rootUrl: _firestoreApiHost.toString()),
74+
projectId,
75+
),
76+
);
8077
}

packages/dart_firebase_admin/lib/src/messaging/fmc_exception.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ const messagingServerToClientCode = {
5656
class FirebaseMessagingAdminException extends FirebaseAdminException
5757
implements Exception {
5858
FirebaseMessagingAdminException(this.errorCode, [String? message])
59-
: super('messaging', errorCode.code, message ?? errorCode.message);
59+
: super(
60+
FirebaseServiceType.messaging.name,
61+
errorCode.code,
62+
message ?? errorCode.message,
63+
);
6064

6165
@internal
6266
factory FirebaseMessagingAdminException.fromServerError({

0 commit comments

Comments
 (0)