Skip to content

Commit 82a05e1

Browse files
committed
adding tests
1 parent 1e1b419 commit 82a05e1

File tree

12 files changed

+419
-11
lines changed

12 files changed

+419
-11
lines changed

example/basic/firebase.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
"ui": {
2626
"enabled": true,
2727
"port": 4000
28+
},
29+
"storage": {
30+
"port": 9199
2831
}
2932
},
3033
"database": {
3134
"rules": "database.rules.json"
35+
},
36+
"storage": {
37+
"rules": "storage.rules"
3238
}
3339
}

example/basic/lib/main.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ void main(List<String> args) async {
425425
// ==========================================================================
426426

427427
// Storage onObjectFinalized - triggers when an object is created/overwritten
428-
firebase.storage.onObjectFinalized(bucket: 'my-bucket', (event) async {
428+
firebase.storage.onObjectFinalized(bucket: 'demo-test.firebasestorage.app', (event) async {
429429
final data = event.data;
430430
print('Object finalized in bucket: ${event.bucket}');
431431
print(' Name: ${data?.name}');
@@ -434,22 +434,22 @@ void main(List<String> args) async {
434434
});
435435

436436
// Storage onObjectArchived - triggers when an object is archived
437-
firebase.storage.onObjectArchived(bucket: 'my-bucket', (event) async {
437+
firebase.storage.onObjectArchived(bucket: 'demo-test.firebasestorage.app', (event) async {
438438
final data = event.data;
439439
print('Object archived in bucket: ${event.bucket}');
440440
print(' Name: ${data?.name}');
441441
print(' Storage Class: ${data?.storageClass}');
442442
});
443443

444444
// Storage onObjectDeleted - triggers when an object is deleted
445-
firebase.storage.onObjectDeleted(bucket: 'my-bucket', (event) async {
445+
firebase.storage.onObjectDeleted(bucket: 'demo-test.firebasestorage.app', (event) async {
446446
final data = event.data;
447447
print('Object deleted in bucket: ${event.bucket}');
448448
print(' Name: ${data?.name}');
449449
});
450450

451451
// Storage onObjectMetadataUpdated - triggers when object metadata changes
452-
firebase.storage.onObjectMetadataUpdated(bucket: 'my-bucket', (
452+
firebase.storage.onObjectMetadataUpdated(bucket: 'demo-test.firebasestorage.app', (
453453
event,
454454
) async {
455455
final data = event.data;

example/basic/storage.rules

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
rules_version = '2';
2+
service firebase.storage {
3+
match /b/{bucket}/o {
4+
match /{allPaths=**} {
5+
allow read, write: if true;
6+
}
7+
}
8+
}

example/client_app/index.html

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,26 +194,47 @@ <h2>6. Raw POST to Callable (without SDK)</h2>
194194
<div id="rawResult" class="result" style="display: none;"></div>
195195
</div>
196196

197+
<!-- Storage Upload Demo -->
198+
<div class="card">
199+
<h2>7. Storage - File Upload (triggers onObjectFinalized)</h2>
200+
<p>Uploads a file to Cloud Storage via the Firebase SDK. The emulator automatically triggers <code>onObjectFinalized</code> on the Dart function. Check the emulator logs to see the trigger fire.</p>
201+
202+
<div>
203+
<input type="file" id="storageFile">
204+
</div>
205+
<div>
206+
<input type="text" id="storagePath" placeholder="Upload path" value="uploads/" style="width: 300px;">
207+
<button onclick="uploadFile()">Upload to Storage</button>
208+
<button onclick="deleteFile()">Delete Last Upload</button>
209+
</div>
210+
211+
<div id="storageResult" class="result" style="display: none;"></div>
212+
</div>
213+
197214
<!-- Firebase SDK -->
198215
<script type="module">
199216
// Import Firebase SDK
200217
import { initializeApp } from 'https://www.gstatic.com/firebasejs/12.8.0/firebase-app.js';
201218
import { getFunctions, httpsCallable, connectFunctionsEmulator } from 'https://www.gstatic.com/firebasejs/12.8.0/firebase-functions.js';
202219
import { getAuth, connectAuthEmulator, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut as firebaseSignOut, onAuthStateChanged } from 'https://www.gstatic.com/firebasejs/12.8.0/firebase-auth.js';
220+
import { getStorage, connectStorageEmulator, ref, uploadBytes, deleteObject, getMetadata } from 'https://www.gstatic.com/firebasejs/12.8.0/firebase-storage.js';
203221

204222
// Initialize Firebase (minimal config for Functions)
205223
const firebaseConfig = {
206224
projectId: 'demo-test',
207225
apiKey: 'fake-api-key', // Not needed for emulator
226+
storageBucket: 'demo-test.firebasestorage.app',
208227
};
209228

210229
const app = initializeApp(firebaseConfig);
211230
const functions = getFunctions(app, 'us-central1');
212231
const auth = getAuth(app);
232+
const storage = getStorage(app);
213233

214234
// Connect to emulators
215235
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
216236
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
237+
connectStorageEmulator(storage, '127.0.0.1', 9199);
217238

218239
// Track auth state
219240
onAuthStateChanged(auth, (user) => {
@@ -234,8 +255,13 @@ <h2>6. Raw POST to Callable (without SDK)</h2>
234255
window.signInWithEmailAndPassword = signInWithEmailAndPassword;
235256
window.createUserWithEmailAndPassword = createUserWithEmailAndPassword;
236257
window.firebaseSignOut = firebaseSignOut;
258+
window.storage = storage;
259+
window.storageRef = ref;
260+
window.uploadBytes = uploadBytes;
261+
window.deleteObject = deleteObject;
262+
window.getMetadata = getMetadata;
237263

238-
console.log('Firebase initialized and connected to emulators (Functions + Auth)');
264+
console.log('Firebase initialized and connected to emulators (Functions + Auth + Storage)');
239265
</script>
240266

241267
<script>
@@ -465,6 +491,64 @@ <h2>6. Raw POST to Callable (without SDK)</h2>
465491
}
466492
}
467493
window.callRawCallable = callRawCallable;
494+
495+
// 7. Storage - File upload (triggers onObjectFinalized)
496+
let lastUploadedRef = null;
497+
498+
async function uploadFile() {
499+
const fileInput = document.getElementById('storageFile');
500+
const pathPrefix = document.getElementById('storagePath').value || 'uploads/';
501+
502+
if (!fileInput.files.length) {
503+
showResult('storageResult', 'Please select a file first.', true);
504+
return;
505+
}
506+
507+
const file = fileInput.files[0];
508+
const fullPath = pathPrefix + file.name;
509+
510+
try {
511+
const fileRef = window.storageRef(window.storage, fullPath);
512+
const snapshot = await window.uploadBytes(fileRef, file, {
513+
contentType: file.type,
514+
});
515+
516+
lastUploadedRef = fileRef;
517+
const metadata = await window.getMetadata(fileRef);
518+
519+
showResult('storageResult', {
520+
status: 'Upload complete - check emulator logs for onObjectFinalized trigger',
521+
path: fullPath,
522+
contentType: metadata.contentType,
523+
size: metadata.size,
524+
bucket: metadata.bucket,
525+
timeCreated: metadata.timeCreated,
526+
});
527+
} catch (error) {
528+
showResult('storageResult', `Upload error: ${error.message}`, true);
529+
}
530+
}
531+
window.uploadFile = uploadFile;
532+
533+
async function deleteFile() {
534+
if (!lastUploadedRef) {
535+
showResult('storageResult', 'No file uploaded yet. Upload a file first.', true);
536+
return;
537+
}
538+
539+
try {
540+
await window.deleteObject(lastUploadedRef);
541+
542+
showResult('storageResult', {
543+
status: 'File deleted - check emulator logs for onObjectDeleted trigger',
544+
path: lastUploadedRef.fullPath,
545+
});
546+
lastUploadedRef = null;
547+
} catch (error) {
548+
showResult('storageResult', `Delete error: ${error.message}`, true);
549+
}
550+
}
551+
window.deleteFile = deleteFile;
468552
</script>
469553
</body>
470554
</html>

lib/builder.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,8 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
541541
final bucketName = node.extractLiteralForArg('bucket');
542542
if (bucketName == null) return;
543543

544-
// Generate function name from bucket (remove hyphens to match Node.js behavior)
545-
final sanitizedBucket = bucketName.replaceAll('-', '');
544+
// Generate function name from bucket (strip non-alphanumeric chars for valid function ID)
545+
final sanitizedBucket = bucketName.replaceAll(RegExp('[^a-zA-Z0-9]'), '');
546546
final functionName = '${methodName}_$sanitizedBucket';
547547

548548
endpoints[functionName] = EndpointSpec(

lib/src/server.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,17 +533,21 @@ Future<(Request, FirebaseFunctionDeclaration?)> _tryMatchCloudEventFunction(
533533
// - google.cloud.storage.object.v1.metadataUpdated
534534
if (type.startsWith('google.cloud.storage.object.v1.')) {
535535
// Extract bucket name from source URL
536+
// Source format: //storage.googleapis.com/projects/_/buckets/{bucket}/objects/{path}
537+
// or just: //storage.googleapis.com/projects/_/buckets/{bucket}
536538
String? bucketName;
537539
if (source.contains('/buckets/')) {
538-
bucketName = source.split('/buckets/').last;
540+
final afterBuckets = source.split('/buckets/').last;
541+
// Bucket name is the first path segment (before any /objects/... suffix)
542+
bucketName = afterBuckets.split('/').first;
539543
}
540544

541545
if (bucketName != null) {
542546
// Map CloudEvent type to method name
543547
final methodName = _mapCloudEventTypeToStorageMethod(type);
544548
if (methodName != null) {
545549
// Sanitize bucket name to match function naming convention
546-
final sanitizedBucket = bucketName.replaceAll('-', '');
550+
final sanitizedBucket = bucketName.replaceAll(RegExp('[^a-zA-Z0-9]'), '');
547551
final expectedFunctionName = '${methodName}_$sanitizedBucket';
548552

549553
// Try to find a matching function

lib/src/storage/storage_namespace.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ class StorageNamespace extends FunctionsNamespace {
159159
/// Examples:
160160
/// - ('onObjectFinalized', 'my-bucket') -> 'onObjectFinalized_mybucket'
161161
String _bucketToFunctionName(String methodName, String bucket) {
162-
final sanitizedBucket = bucket.replaceAll('-', '');
162+
final sanitizedBucket = bucket.replaceAll(RegExp('[^a-zA-Z0-9]'), '');
163163
return '${methodName}_$sanitizedBucket';
164164
}
165165

test/e2e/e2e_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import 'helpers/emulator.dart';
1313
import 'helpers/firestore_client.dart';
1414
import 'helpers/http_client.dart';
1515
import 'helpers/pubsub_client.dart';
16+
import 'helpers/storage_client.dart';
1617
import 'tests/database_tests.dart';
1718
import 'tests/firestore_tests.dart';
1819
import 'tests/https_onrequest_tests.dart';
1920
import 'tests/identity_tests.dart';
2021
import 'tests/integration_tests.dart';
2122
import 'tests/pubsub_tests.dart';
2223
import 'tests/scheduler_tests.dart';
24+
import 'tests/storage_tests.dart';
2325

2426
void main() {
2527
late EmulatorHelper emulator;
@@ -28,6 +30,7 @@ void main() {
2830
late FirestoreClient firestoreClient;
2931
late DatabaseClient databaseClient;
3032
late AuthClient authClient;
33+
late StorageClient storageClient;
3134

3235
// Debug: Show Directory.current.path at module load time
3336
print('DEBUG e2e_test: Directory.current.path = ${Directory.current.path}');
@@ -90,6 +93,12 @@ void main() {
9093
// Create Auth client
9194
authClient = AuthClient(emulator.authUrl, 'demo-test');
9295

96+
// Create Storage client
97+
storageClient = StorageClient(
98+
emulator.storageUrl,
99+
'demo-test.firebasestorage.app',
100+
);
101+
93102
// Give emulator a moment to fully initialize
94103
await Future<void>.delayed(const Duration(seconds: 2));
95104
});
@@ -106,6 +115,7 @@ void main() {
106115
firestoreClient.close();
107116
databaseClient.close();
108117
authClient.close();
118+
storageClient.close();
109119
await emulator.stop();
110120
});
111121

@@ -116,6 +126,7 @@ void main() {
116126
runFirestoreTests(() => examplePath, () => firestoreClient, () => emulator);
117127
runDatabaseTests(() => examplePath, () => databaseClient, () => emulator);
118128
runIdentityTests(() => examplePath, () => authClient, () => emulator);
129+
runStorageTests(() => examplePath, () => storageClient, () => emulator);
119130
runSchedulerTests(
120131
() => examplePath,
121132
() => emulator,

test/e2e/helpers/emulator.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class EmulatorHelper {
1111
this.firestorePort = 8080,
1212
this.databasePort = 9000,
1313
this.authPort = 9099,
14+
this.storagePort = 9199,
1415
this.startupTimeout = const Duration(seconds: 90),
1516
});
1617
Process? _process;
@@ -20,6 +21,7 @@ class EmulatorHelper {
2021
final int firestorePort;
2122
final int databasePort;
2223
final int authPort;
24+
final int storagePort;
2325
final Duration startupTimeout;
2426

2527
// Completer to signal when emulator is ready
@@ -56,7 +58,7 @@ class EmulatorHelper {
5658
'emulators:start',
5759
'--debug',
5860
'--only',
59-
'functions,pubsub,firestore,database,auth',
61+
'functions,pubsub,firestore,database,auth,storage',
6062
'--project',
6163
'demo-test',
6264
'--non-interactive',
@@ -278,6 +280,9 @@ class EmulatorHelper {
278280
/// Gets the base URL for the Auth emulator REST API.
279281
String get authUrl => 'http://localhost:$authPort';
280282

283+
/// Gets the base URL for the Storage emulator REST API.
284+
String get storageUrl => 'http://localhost:$storagePort';
285+
281286
/// Verifies that a function was executed in the emulator logs.
282287
/// Returns true if we find both "Beginning execution" and "Finished" messages.
283288
bool verifyFunctionExecution(String functionName) {

0 commit comments

Comments
 (0)