From e5ad7ee3f5f119c8611b88e02e683a6daa96dfb1 Mon Sep 17 00:00:00 2001 From: Mac Date: Tue, 19 May 2026 13:58:50 +0100 Subject: [PATCH] Fix Flutter realtime message list parsing --- src/SDK/Language/Flutter.php | 5 + .../lib/src/realtime_message.dart.twig | 38 ++++- .../test/src/realtime_message_test.dart.twig | 157 ++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 templates/flutter/test/src/realtime_message_test.dart.twig diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index a17254a332..c9c5391c5c 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -320,6 +320,11 @@ public function getFiles(): array 'destination' => '/test/src/realtime_response_test.dart', 'template' => 'flutter/test/src/realtime_response_test.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/test/src/realtime_message_test.dart', + 'template' => 'flutter/test/src/realtime_message_test.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/test/src/realtime_response_connected_test.dart', diff --git a/templates/flutter/lib/src/realtime_message.dart.twig b/templates/flutter/lib/src/realtime_message.dart.twig index 9dc0423f75..d17ba92080 100644 --- a/templates/flutter/lib/src/realtime_message.dart.twig +++ b/templates/flutter/lib/src/realtime_message.dart.twig @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; /// Realtime Message class RealtimeMessage { /// All permutations of the system event that triggered this message - /// + /// /// The first event in the list is the most specfic event without wildcards. final List events; @@ -54,13 +54,45 @@ class RealtimeMessage { /// Initializes a [RealtimeMessage] from a [Map]. factory RealtimeMessage.fromMap(Map map) { return RealtimeMessage( - events: List.from(map['events'] ?? []), + events: _stringListFrom(map['events']), payload: Map.from(map['payload'] ?? {}), - channels: List.from(map['channels'] ?? []), + channels: _stringListFrom(map['channels']), timestamp: map['timestamp'], ); } + static List _stringListFrom(dynamic value) { + if (value == null) { + return []; + } + + if (value is Iterable) { + return List.from(value); + } + + if (value is Map) { + if (value.keys.every(_isArrayIndexKey)) { + return List.from(value.values); + } + + return List.from(value.keys); + } + + return []; + } + + static bool _isArrayIndexKey(dynamic key) { + if (key is int) { + return key >= 0; + } + + if (key is String) { + return int.tryParse(key) != null; + } + + return false; + } + /// Converts a [RealtimeMessage] to a JSON [String]. String toJson() => json.encode(toMap()); diff --git a/templates/flutter/test/src/realtime_message_test.dart.twig b/templates/flutter/test/src/realtime_message_test.dart.twig new file mode 100644 index 0000000000..9fa89fc040 --- /dev/null +++ b/templates/flutter/test/src/realtime_message_test.dart.twig @@ -0,0 +1,157 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{language.params.packageName}}/src/realtime_message.dart'; + +void main() { + group('RealtimeMessage', () { + final events = ['databases.*.collections.*.documents.*.create']; + final payload = {'\$id': 'message-id'}; + final channels = ['databases.default.collections.messages.documents']; + final timestamp = '2026-02-26T12:00:00.000+00:00'; + final message = RealtimeMessage( + events: events, + payload: payload, + channels: channels, + timestamp: timestamp, + ); + + test('toMap should return a map representation of the message', () { + final messageMap = message.toMap(); + + expect(messageMap['events'], equals(events)); + expect(messageMap['payload'], equals(payload)); + expect(messageMap['channels'], equals(channels)); + expect(messageMap['timestamp'], equals(timestamp)); + }); + + test('fromMap should create an instance from lists', () { + final messageMap = { + 'events': events, + 'payload': payload, + 'channels': channels, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, equals(events)); + expect(result.payload, equals(payload)); + expect(result.channels, equals(channels)); + expect(result.timestamp, equals(timestamp)); + }); + + test('fromMap should create an instance from maps', () { + final messageMap = { + 'events': {'0': events.first}, + 'payload': payload, + 'channels': {'0': channels.first}, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, equals(events)); + expect(result.payload, equals(payload)); + expect(result.channels, equals(channels)); + expect(result.timestamp, equals(timestamp)); + }); + + test('fromMap should use map values when map keys are integers', () { + final messageMap = { + 'events': {0: events.first}, + 'payload': payload, + 'channels': {0: channels.first}, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, equals(events)); + expect(result.channels, equals(channels)); + }); + + test('fromMap should use map keys when map values are not strings', () { + final messageMap = { + 'events': {events.first: true}, + 'payload': payload, + 'channels': {channels.first: true}, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, equals(events)); + expect(result.channels, equals(channels)); + }); + + test('fromMap should use map keys for mixed-value maps', () { + final messageMap = { + 'events': { + events.first: 'someDesc', + 'databases.*.collections.*.documents.*.update': null, + }, + 'payload': payload, + 'channels': {channels.first: 'someDesc'}, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect( + result.events, + equals([events.first, 'databases.*.collections.*.documents.*.update']), + ); + expect(result.channels, equals(channels)); + }); + + test( + 'fromMap should use empty lists when events and channels are null', + () { + final messageMap = { + 'events': null, + 'payload': payload, + 'channels': null, + 'timestamp': timestamp, + }; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, isEmpty); + expect(result.channels, isEmpty); + }, + ); + + test( + 'fromMap should use empty lists when events and channels are absent', + () { + final messageMap = {'payload': payload, 'timestamp': timestamp}; + + final result = RealtimeMessage.fromMap(messageMap); + + expect(result.events, isEmpty); + expect(result.channels, isEmpty); + }, + ); + + test('fromMap should throw when list items are not strings', () { + final messageMap = { + 'events': [true], + 'payload': payload, + 'channels': channels, + 'timestamp': timestamp, + }; + + expect( + () => RealtimeMessage.fromMap(messageMap), + throwsA(isA()), + ); + }); + + test('toJson and fromJson should convert to/from JSON', () { + final jsonString = message.toJson(); + + final result = RealtimeMessage.fromJson(jsonString); + + expect(result, equals(message)); + }); + }); +}