From ff3efaf0280b1e680b3e439d80b9046b29ae7b49 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Tue, 29 Oct 2024 10:05:48 -0700 Subject: [PATCH] simplify mock server serialization and add test for multiple realtime events --- ...ffline_first_with_supabase_repository.dart | 14 ++--- ...e_first_with_supabase_repository_test.dart | 54 ++++++++++++++++--- .../lib/src/testing/supabase_mock_server.dart | 21 ++------ .../lib/src/testing/supabase_response.dart | 43 +++++++-------- 4 files changed, 79 insertions(+), 53 deletions(-) diff --git a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart index 70249d3f..9f59cbd8 100644 --- a/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart +++ b/packages/brick_offline_first_with_supabase/lib/src/offline_first_with_supabase_repository.dart @@ -157,14 +157,14 @@ abstract class OfflineFirstWithSupabaseRepository Map payload, { required Map supabaseDefinitions, }) { - final fieldsWithValues = payload.entries.fold({}, (acc, entry) { - for (final f in supabaseDefinitions.entries) { - if (f.value.columnName == entry.key) { - acc[f.key] = entry.value; - break; - } - } + final columnsToFields = supabaseDefinitions.entries.fold({}, (acc, entry) { + acc[entry.value.columnName] = entry.key; + return acc; + }); + final fieldsWithValues = payload.entries.fold({}, (acc, entry) { + if (columnsToFields[entry.key] == null) return acc; + acc[columnsToFields[entry.key]!] = entry.value; return acc; }); diff --git a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart index 1b785499..34e0ddc7 100644 --- a/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart +++ b/packages/brick_offline_first_with_supabase/test/offline_first_with_supabase_repository_test.dart @@ -444,7 +444,6 @@ void main() async { realtimeEvent: PostgresChangeEvent.insert, repository: repository, ), - realtimeEvent: PostgresChangeEvent.insert, ); mock.handle({req: resp}); @@ -486,7 +485,6 @@ void main() async { realtimeEvent: PostgresChangeEvent.delete, repository: repository, ), - realtimeEvent: PostgresChangeEvent.delete, ); mock.handle({req: resp}); @@ -536,7 +534,6 @@ void main() async { realtimeEvent: PostgresChangeEvent.update, repository: repository, ), - realtimeEvent: PostgresChangeEvent.update, ); mock.handle({req: resp}); }); @@ -572,7 +569,6 @@ void main() async { realtimeEvent: PostgresChangeEvent.insert, repository: repository, ), - realtimeEvent: PostgresChangeEvent.insert, ); mock.handle({req: resp}); @@ -613,7 +609,6 @@ void main() async { realtimeEvent: PostgresChangeEvent.delete, repository: repository, ), - realtimeEvent: PostgresChangeEvent.delete, ); mock.handle({req: resp}); @@ -662,7 +657,54 @@ void main() async { realtimeEvent: PostgresChangeEvent.update, repository: repository, ), - realtimeEvent: PostgresChangeEvent.update, + ); + mock.handle({req: resp}); + }); + + test('with multiple events', () async { + final customer1 = Customer( + id: 1, + firstName: 'Thomas', + lastName: 'Guy', + pizzas: [ + Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false), + ], + ); + final customer2 = Customer( + id: 1, + firstName: 'Guy', + lastName: 'Thomas', + pizzas: [ + Pizza(id: 2, toppings: [Topping.pepperoni], frozen: false), + ], + ); + + final customers = repository.subscribeToRealtime(); + expect( + customers, + emitsInOrder([ + [], + [customer1], + [customer2], + ]), + ); + + final req = SupabaseRequest(); + final resp = SupabaseResponse( + await mock.serialize( + customer1, + realtimeEvent: PostgresChangeEvent.insert, + repository: repository, + ), + realtimeSubsequentReplies: [ + SupabaseResponse( + await mock.serialize( + customer2, + realtimeEvent: PostgresChangeEvent.update, + repository: repository, + ), + ), + ], ); mock.handle({req: resp}); }); diff --git a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart index 5dc8bc29..60e8bce0 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_mock_server.dart @@ -107,8 +107,8 @@ class SupabaseMockServer { final data = Map.from(r.data as Map); return { - 'id': _realtimeEventToId(r.realtimeEvent!), - 'event': r.realtimeEvent!.name.toUpperCase(), + 'id': data['payload']['ids'][0], + 'event': data['payload']['data']['type'], 'schema': data['payload']['data']['schema'], 'table': data['payload']['data']['table'], if (realtimeFilter != null) 'filter': realtimeFilter, @@ -124,7 +124,7 @@ class SupabaseMockServer { } for (final realtimeResponses in matching.value.flattenedResponses) { - await Future.delayed(matching.value.realtimeDelayBetweenResponses); + await Future.delayed(matching.value.realtimeSubsequentReplyDelay); final data = Map.from(realtimeResponses.data as Map); final serialized = jsonEncode({...data, 'topic': topic}); webSocket!.add(serialized); @@ -177,19 +177,6 @@ class SupabaseMockServer { return resp; } - int _realtimeEventToId(PostgresChangeEvent event) { - switch (event) { - case PostgresChangeEvent.insert: - return 77086988; - case PostgresChangeEvent.update: - return 25993878; - case PostgresChangeEvent.delete: - return 48673474; - case PostgresChangeEvent.all: - return 0; - } - } - /// Convert a model to a Supabase response. /// /// For realtime responses, include the `realtimeEvent` argument. @@ -224,7 +211,7 @@ class SupabaseMockServer { 'ref': null, 'event': 'postgres_changes', 'payload': { - 'ids': [_realtimeEventToId(realtimeEvent)], + 'ids': [realtimeEvent.index], 'data': { 'columns': adapter.fieldsToSupabaseColumns.entries.map((entry) { return {'name': entry.value.columnName, 'type': 'text', 'type_modifier': 4294967295}; diff --git a/packages/brick_supabase/lib/src/testing/supabase_response.dart b/packages/brick_supabase/lib/src/testing/supabase_response.dart index daf64526..0e9f7033 100644 --- a/packages/brick_supabase/lib/src/testing/supabase_response.dart +++ b/packages/brick_supabase/lib/src/testing/supabase_response.dart @@ -1,28 +1,14 @@ -import 'package:supabase/supabase.dart'; - class SupabaseResponse { final dynamic data; - final Map? headers; - - final Duration realtimeDelayBetweenResponses; - - final PostgresChangeEvent? realtimeEvent; - - final List realtimeResponses; - + /// All recursively-discovered [realtimeSubsequentReplies] List get flattenedResponses { - final starter = []; - if (realtimeEvent != PostgresChangeEvent.all) { - starter.add(this); - } - return realtimeResponses.fold(starter, (acc, r) { + return realtimeSubsequentReplies.fold([this], (acc, r) { void recurse(SupabaseResponse response) { - if (response.realtimeResponses.isNotEmpty) { - acc.addAll( - response.realtimeResponses.where((e) => e.realtimeEvent != PostgresChangeEvent.all), - ); - response.realtimeResponses.forEach(recurse); + acc.add(response); + if (response.realtimeSubsequentReplies.isNotEmpty) { + acc.addAll(response.realtimeSubsequentReplies); + response.realtimeSubsequentReplies.forEach(recurse); } } @@ -31,11 +17,22 @@ class SupabaseResponse { }); } + final Map? headers; + + /// Additional replies sent after this instance's [data]. + /// Replies will be staggered by [realtimeSubsequentReplyDelay]. + /// + /// While [flattenedResponses] supports recursion, it should never be + /// necessary to have deeply nested responses. + final List realtimeSubsequentReplies; + + /// Amount of time to delay each [realtimeSubsequentReplies] + final Duration realtimeSubsequentReplyDelay; + SupabaseResponse( this.data, { this.headers, - this.realtimeEvent, - this.realtimeDelayBetweenResponses = const Duration(milliseconds: 10), - this.realtimeResponses = const [], + this.realtimeSubsequentReplies = const [], + this.realtimeSubsequentReplyDelay = const Duration(milliseconds: 10), }); }