diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java index 5bf04d188..4a7da1ee7 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java @@ -141,7 +141,6 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) { final String eventName = eventMessage.eventName; final Map eventPayload = (eventMessage.message == null) ? null : (Map) eventMessage.message; try { - ChannelOptions channelOptions; switch (eventName) { case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: connectionStateListener = new PluginConnectionStateListener(eventSink); @@ -149,32 +148,22 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) { break; case PlatformConstants.PlatformMethod.onRealtimeChannelStateChanged: assert eventPayload != null : "event message is missing"; - try { - final Channel channel = ablyLibrary + channelStateListener = new PluginChannelStateListener(eventSink); + ablyLibrary .getRealtime(ablyMessage.handle) .channels - .get( - (String) eventPayload.get("channel"), - (ChannelOptions) eventPayload.get("options") - ); - channelStateListener = new PluginChannelStateListener(eventSink); - channel.on(channelStateListener); - } catch (AblyException ablyException) { - handleAblyException(eventSink, ablyException); - } + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .on(channelStateListener); break; case PlatformConstants.PlatformMethod.onRealtimeChannelMessage: assert eventPayload != null : "event message is missing"; try { - final Channel channel = ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get( - (String) eventPayload.get("channel"), - (ChannelOptions) eventPayload.get("options") - ); channelMessageListener = new PluginChannelMessageListener(eventSink); - channel.subscribe(channelMessageListener); + ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .subscribe(channelMessageListener); } catch (AblyException ablyException) { handleAblyException(eventSink, ablyException); } @@ -182,12 +171,12 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) { case PlatformConstants.PlatformMethod.onRealtimePresenceMessage: assert eventPayload != null : "event message is missing"; try { - final Channel channel = ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)); channelPresenceMessageListener = new PluginChannelPresenceMessageListener(eventSink); - channel.presence.subscribe(channelPresenceMessageListener); + ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .presence.subscribe(channelPresenceMessageListener); } catch (AblyException ablyException) { handleAblyException(eventSink, ablyException); } @@ -219,27 +208,27 @@ public void onCancel(Object object) { // left as is as there is no way of propagating this error to flutter side assert eventPayload != null : "event message is missing"; ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get((String) eventPayload.get("channel")) - .off(channelStateListener); + .getRealtime(ablyMessage.handle) + .channels + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .off(channelStateListener); break; case PlatformConstants.PlatformMethod.onRealtimeChannelMessage: assert eventPayload != null : "event message is missing"; ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get((String) eventPayload.get("channel")) - .unsubscribe(channelMessageListener); + .getRealtime(ablyMessage.handle) + .channels + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .unsubscribe(channelMessageListener); break; case PlatformConstants.PlatformMethod.onRealtimePresenceMessage: assert eventPayload != null : "event message is missing"; ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) - .presence - .unsubscribe(channelPresenceMessageListener); + .getRealtime(ablyMessage.handle) + .channels + .get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName)) + .presence + .unsubscribe(channelPresenceMessageListener); break; } } diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java index 622028d28..b38ec8eea 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java @@ -22,7 +22,9 @@ import io.ably.lib.realtime.ConnectionStateListener; import io.ably.lib.rest.Auth; import io.ably.lib.rest.Auth.TokenDetails; +import io.ably.lib.types.AblyException; import io.ably.lib.types.AsyncPaginatedResult; +import io.ably.lib.types.ChannelOptions; import io.ably.lib.types.ClientOptions; import io.ably.lib.types.ErrorInfo; import io.ably.lib.types.Message; @@ -74,39 +76,43 @@ T decode(Map jsonMap) { codecMap = new HashMap() { { put(PlatformConstants.CodecTypes.ablyMessage, - new CodecPair<>(self::encodeAblyFlutterMessage, self::decodeAblyFlutterMessage)); + new CodecPair<>(self::encodeAblyFlutterMessage, self::decodeAblyFlutterMessage)); put(PlatformConstants.CodecTypes.ablyEventMessage, - new CodecPair<>(null, self::decodeAblyFlutterEventMessage)); + new CodecPair<>(null, self::decodeAblyFlutterEventMessage)); put(PlatformConstants.CodecTypes.clientOptions, - new CodecPair<>(null, self::decodeClientOptions)); + new CodecPair<>(null, self::decodeClientOptions)); put(PlatformConstants.CodecTypes.tokenParams, - new CodecPair<>(self::encodeTokenParams, null)); + new CodecPair<>(self::encodeTokenParams, null)); put(PlatformConstants.CodecTypes.tokenDetails, - new CodecPair<>(null, self::decodeTokenDetails)); + new CodecPair<>(null, self::decodeTokenDetails)); put(PlatformConstants.CodecTypes.tokenRequest, - new CodecPair<>(null, self::decodeTokenRequest)); + new CodecPair<>(null, self::decodeTokenRequest)); + put(PlatformConstants.CodecTypes.restChannelOptions, + new CodecPair<>(null, self::decodeRestChannelOptions)); + put(PlatformConstants.CodecTypes.realtimeChannelOptions, + new CodecPair<>(null, self::decodeRealtimeChannelOptions)); put(PlatformConstants.CodecTypes.paginatedResult, - new CodecPair<>(self::encodePaginatedResult, null)); + new CodecPair<>(self::encodePaginatedResult, null)); put(PlatformConstants.CodecTypes.restHistoryParams, - new CodecPair<>(null, self::decodeRestHistoryParams)); + new CodecPair<>(null, self::decodeRestHistoryParams)); put(PlatformConstants.CodecTypes.realtimeHistoryParams, - new CodecPair<>(null, self::decodeRealtimeHistoryParams)); + new CodecPair<>(null, self::decodeRealtimeHistoryParams)); put(PlatformConstants.CodecTypes.restPresenceParams, - new CodecPair<>(null, self::decodeRestPresenceParams)); + new CodecPair<>(null, self::decodeRestPresenceParams)); put(PlatformConstants.CodecTypes.realtimePresenceParams, - new CodecPair<>(null, self::decodeRealtimePresenceParams)); + new CodecPair<>(null, self::decodeRealtimePresenceParams)); put(PlatformConstants.CodecTypes.errorInfo, - new CodecPair<>(self::encodeErrorInfo, null)); + new CodecPair<>(self::encodeErrorInfo, null)); put(PlatformConstants.CodecTypes.messageData, - new CodecPair<>(null, self::decodeChannelMessageData)); + new CodecPair<>(null, self::decodeChannelMessageData)); put(PlatformConstants.CodecTypes.message, - new CodecPair<>(self::encodeChannelMessage, self::decodeChannelMessage)); + new CodecPair<>(self::encodeChannelMessage, self::decodeChannelMessage)); put(PlatformConstants.CodecTypes.presenceMessage, - new CodecPair<>(self::encodePresenceMessage, null)); + new CodecPair<>(self::encodePresenceMessage, null)); put(PlatformConstants.CodecTypes.connectionStateChange, - new CodecPair<>(self::encodeConnectionStateChange, null)); + new CodecPair<>(self::encodeConnectionStateChange, null)); put(PlatformConstants.CodecTypes.channelStateChange, - new CodecPair<>(self::encodeChannelStateChange, null)); + new CodecPair<>(self::encodeChannelStateChange, null)); } }; } @@ -314,6 +320,32 @@ private Auth.TokenRequest decodeTokenRequest(Map jsonMap) { return o; } + private ChannelOptions decodeRestChannelOptions(Map jsonMap) { + if (jsonMap == null) return null; + final Object cipher = jsonMap.get(PlatformConstants.TxRealtimeChannelOptions.cipher); + try { + return ChannelOptions.withCipherKey((String) cipher); + } catch (AblyException ae) { + System.out.println("Exception while decoding RestChannelOptions : " + ae); + return null; + } + } + + private ChannelOptions decodeRealtimeChannelOptions(Map jsonMap) { + if (jsonMap == null) return null; + final Object cipher = jsonMap.get(PlatformConstants.TxRealtimeChannelOptions.cipher); + try { + final ChannelOptions o = ChannelOptions.withCipherKey((String) cipher); + readValueFromJson(jsonMap, PlatformConstants.TxRealtimeChannelOptions.params, v -> o.cipherParams = (Map) v); + // modes is not supported in ably-java + // Track @ https://github.com/ably/ably-flutter/issues/14 + return o; + } catch (AblyException ae) { + System.out.println("Exception while decoding RealtimeChannelOptions: " + ae); + return null; + } + } + private Param[] decodeRestHistoryParams(Map jsonMap) { if (jsonMap == null) return null; Param[] params = new Param[jsonMap.size()]; @@ -573,9 +605,9 @@ private Map encodePaginatedResult(AsyncPaginatedResult c list.add((Map) pair.encode(item)); } writeValueToJson( - jsonMap, - PlatformConstants.TxPaginatedResult.items, - list + jsonMap, + PlatformConstants.TxPaginatedResult.items, + list ); writeValueToJson(jsonMap, PlatformConstants.TxPaginatedResult.type, type & 0xff); } diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java index e4f270990..9aad9106a 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java @@ -31,548 +31,585 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler { - private static AblyMethodCallHandler _instance; - - public interface OnHotRestart { - // this method will be called on every fresh start and on every hot restart - void trigger(); - } - - static synchronized AblyMethodCallHandler getInstance(final MethodChannel channel, final OnHotRestart listener) { - if (null == _instance) { - _instance = new AblyMethodCallHandler(channel, listener); - } - return _instance; - } - - private final MethodChannel channel; - private final OnHotRestart onHotRestartListener; - private final Map> _map; - private final AblyLibrary _ably = AblyLibrary.getInstance(); - - private AblyMethodCallHandler(final MethodChannel channel, final OnHotRestart listener) { - this.channel = channel; - this.onHotRestartListener = listener; - _map = new HashMap<>(); - _map.put(PlatformConstants.PlatformMethod.getPlatformVersion, this::getPlatformVersion); - _map.put(PlatformConstants.PlatformMethod.getVersion, this::getVersion); - _map.put(PlatformConstants.PlatformMethod.registerAbly, this::register); - - // Rest - _map.put(PlatformConstants.PlatformMethod.createRestWithOptions, this::createRestWithOptions); - _map.put(PlatformConstants.PlatformMethod.publish, this::publishRestMessage); - _map.put(PlatformConstants.PlatformMethod.restHistory, this::getRestHistory); - _map.put(PlatformConstants.PlatformMethod.restPresenceGet, this::getRestPresence); - _map.put(PlatformConstants.PlatformMethod.restPresenceHistory, this::getRestPresenceHistory); - - //Realtime - _map.put(PlatformConstants.PlatformMethod.createRealtimeWithOptions, this::createRealtimeWithOptions); - _map.put(PlatformConstants.PlatformMethod.connectRealtime, this::connectRealtime); - _map.put(PlatformConstants.PlatformMethod.closeRealtime, this::closeRealtime); - _map.put(PlatformConstants.PlatformMethod.attachRealtimeChannel, this::attachRealtimeChannel); - _map.put(PlatformConstants.PlatformMethod.detachRealtimeChannel, this::detachRealtimeChannel); - _map.put(PlatformConstants.PlatformMethod.setRealtimeChannelOptions, this::setRealtimeChannelOptions); - _map.put(PlatformConstants.PlatformMethod.publishRealtimeChannelMessage, this::publishRealtimeChannelMessage); - _map.put(PlatformConstants.PlatformMethod.realtimeHistory, this::getRealtimeHistory); - _map.put(PlatformConstants.PlatformMethod.realtimePresenceGet, this::getRealtimePresence); - _map.put(PlatformConstants.PlatformMethod.realtimePresenceHistory, this::getRealtimePresenceHistory); - _map.put(PlatformConstants.PlatformMethod.realtimePresenceEnter, this::enterRealtimePresence); - _map.put(PlatformConstants.PlatformMethod.realtimePresenceUpdate, this::updateRealtimePresence); - _map.put(PlatformConstants.PlatformMethod.realtimePresenceLeave, this::leaveRealtimePresence); - - // paginated results - _map.put(PlatformConstants.PlatformMethod.nextPage, this::getNextPage); - _map.put(PlatformConstants.PlatformMethod.firstPage, this::getFirstPage); - } - - // MethodChannel.Result wrapper that responds on the platform thread. - // - // Plugins crash with "Methods marked with @UiThread must be executed on the main thread." - // This happens while making network calls in thread other than main thread - // - // https://github.com/flutter/flutter/issues/34993#issue-459900986 - // https://github.com/aloisdeniel/flutter_geocoder/commit/bc34cfe473bfd1934fe098bb7053248b75200241 - private static class MethodResultWrapper implements MethodChannel.Result { - private final MethodChannel.Result methodResult; - private final Handler handler; - - MethodResultWrapper(MethodChannel.Result result) { - methodResult = result; - handler = new Handler(Looper.getMainLooper()); - } - - @Override - public void success(final Object result) { - handler.post(() -> methodResult.success(result)); - } - - @Override - public void error( - final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails)); - } - - @Override - public void notImplemented() { - handler.post(methodResult::notImplemented); - } + private static AblyMethodCallHandler _instance; + + public interface OnHotRestart { + // this method will be called on every fresh start and on every hot restart + void trigger(); + } + + static synchronized AblyMethodCallHandler getInstance(final MethodChannel channel, final OnHotRestart listener) { + if (null == _instance) { + _instance = new AblyMethodCallHandler(channel, listener); + } + return _instance; + } + + private final MethodChannel channel; + private final OnHotRestart onHotRestartListener; + private final Map> _map; + private final AblyLibrary _ably = AblyLibrary.getInstance(); + + private AblyMethodCallHandler(final MethodChannel channel, final OnHotRestart listener) { + this.channel = channel; + this.onHotRestartListener = listener; + _map = new HashMap<>(); + _map.put(PlatformConstants.PlatformMethod.getPlatformVersion, this::getPlatformVersion); + _map.put(PlatformConstants.PlatformMethod.getVersion, this::getVersion); + _map.put(PlatformConstants.PlatformMethod.registerAbly, this::register); + + // Rest + _map.put(PlatformConstants.PlatformMethod.createRestWithOptions, this::createRestWithOptions); + _map.put(PlatformConstants.PlatformMethod.setRestChannelOptions, this::setRestChannelOptions); + _map.put(PlatformConstants.PlatformMethod.publish, this::publishRestMessage); + _map.put(PlatformConstants.PlatformMethod.restHistory, this::getRestHistory); + _map.put(PlatformConstants.PlatformMethod.restPresenceGet, this::getRestPresence); + _map.put(PlatformConstants.PlatformMethod.restPresenceHistory, this::getRestPresenceHistory); + _map.put(PlatformConstants.PlatformMethod.releaseRestChannel, this::releaseRestChannel); + + //Realtime + _map.put(PlatformConstants.PlatformMethod.createRealtimeWithOptions, this::createRealtimeWithOptions); + _map.put(PlatformConstants.PlatformMethod.connectRealtime, this::connectRealtime); + _map.put(PlatformConstants.PlatformMethod.closeRealtime, this::closeRealtime); + _map.put(PlatformConstants.PlatformMethod.attachRealtimeChannel, this::attachRealtimeChannel); + _map.put(PlatformConstants.PlatformMethod.detachRealtimeChannel, this::detachRealtimeChannel); + _map.put(PlatformConstants.PlatformMethod.setRealtimeChannelOptions, this::setRealtimeChannelOptions); + _map.put(PlatformConstants.PlatformMethod.publishRealtimeChannelMessage, this::publishRealtimeChannelMessage); + _map.put(PlatformConstants.PlatformMethod.realtimeHistory, this::getRealtimeHistory); + _map.put(PlatformConstants.PlatformMethod.realtimePresenceGet, this::getRealtimePresence); + _map.put(PlatformConstants.PlatformMethod.realtimePresenceHistory, this::getRealtimePresenceHistory); + _map.put(PlatformConstants.PlatformMethod.realtimePresenceEnter, this::enterRealtimePresence); + _map.put(PlatformConstants.PlatformMethod.realtimePresenceUpdate, this::updateRealtimePresence); + _map.put(PlatformConstants.PlatformMethod.realtimePresenceLeave, this::leaveRealtimePresence); + _map.put(PlatformConstants.PlatformMethod.releaseRealtimeChannel, this::releaseRealtimeChannel); + + // paginated results + _map.put(PlatformConstants.PlatformMethod.nextPage, this::getNextPage); + _map.put(PlatformConstants.PlatformMethod.firstPage, this::getFirstPage); + } + + // MethodChannel.Result wrapper that responds on the platform thread. + // + // Plugins crash with "Methods marked with @UiThread must be executed on the main thread." + // This happens while making network calls in thread other than main thread + // + // https://github.com/flutter/flutter/issues/34993#issue-459900986 + // https://github.com/aloisdeniel/flutter_geocoder/commit/bc34cfe473bfd1934fe098bb7053248b75200241 + private static class MethodResultWrapper implements MethodChannel.Result { + private final MethodChannel.Result methodResult; + private final Handler handler; + + MethodResultWrapper(MethodChannel.Result result) { + methodResult = result; + handler = new Handler(Looper.getMainLooper()); } - private void handleAblyException(@NonNull MethodChannel.Result result, @NonNull AblyException e) { - result.error(Integer.toString(e.errorInfo.code), e.getMessage(), e.errorInfo); + @Override + public void success(final Object result) { + handler.post(() -> methodResult.success(result)); } @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result rawResult) { - final MethodChannel.Result result = new MethodResultWrapper(rawResult); - System.out.println("Ably Plugin handle: " + call.method); - final BiConsumer handler = _map.get(call.method); - if (null == handler) { - // We don't have a handler for a method with this name so tell the caller. - result.notImplemented(); - } else { - // We have a handler for a method with this name so delegate to it. - handler.accept(call, result); - } - } - - private CompletionListener handleCompletionWithListener(@NonNull MethodChannel.Result result) { - return new CompletionListener() { - @Override - public void onSuccess() { - result.success(null); - } - - @Override - public void onError(ErrorInfo reason) { - handleAblyException(result, AblyException.fromErrorInfo(reason)); - } - }; + public void error( + final String errorCode, final String errorMessage, final Object errorDetails) { + handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails)); } - private void register(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - System.out.println("Registering library instance to clean up any existing instances"); - onHotRestartListener.trigger(); - _ably.dispose(); + @Override + public void notImplemented() { + handler.post(methodResult::notImplemented); + } + } + + private void handleAblyException(@NonNull MethodChannel.Result result, @NonNull AblyException e) { + result.error(Integer.toString(e.errorInfo.code), e.getMessage(), e.errorInfo); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result rawResult) { + final MethodChannel.Result result = new MethodResultWrapper(rawResult); + System.out.println("Ably Plugin handle: " + call.method); + final BiConsumer handler = _map.get(call.method); + if (null == handler) { + // We don't have a handler for a method with this name so tell the caller. + result.notImplemented(); + } else { + // We have a handler for a method with this name so delegate to it. + handler.accept(call, result); + } + } + + private CompletionListener handleCompletionWithListener(@NonNull MethodChannel.Result result) { + return new CompletionListener() { + @Override + public void onSuccess() { result.success(null); - } - - private void createRestWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.ablyDo(message, (ablyLibrary, clientOptions) -> { - try { - final long handle = ablyLibrary.getCurrentHandle(); - if (clientOptions.hasAuthCallback) { - clientOptions.options.authCallback = (Auth.TokenParams params) -> { - Object token = ablyLibrary.getRestToken(handle); - if (token != null) return token; - new Handler(Looper.getMainLooper()).post(() -> { - AblyFlutterMessage channelMessage = new AblyFlutterMessage<>(params, handle); - channel.invokeMethod(PlatformConstants.PlatformMethod.authCallback, channelMessage, new MethodChannel.Result() { - @Override - public void success(@Nullable Object result) { - ablyLibrary.setRestToken(handle, result); - } - - @Override - public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - System.out.println(errorDetails); - //Do nothing, let another request go to flutter side! - } - - @Override - public void notImplemented() { - System.out.println("`authCallback` Method not implemented on dart side"); - } - }); - }); - - return null; - }; + } + + @Override + public void onError(ErrorInfo reason) { + handleAblyException(result, AblyException.fromErrorInfo(reason)); + } + }; + } + + private void register(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + System.out.println("Registering library instance to clean up any existing instances"); + onHotRestartListener.trigger(); + _ably.dispose(); + result.success(null); + } + + private void createRestWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.ablyDo(message, (ablyLibrary, clientOptions) -> { + try { + final long handle = ablyLibrary.getCurrentHandle(); + if (clientOptions.hasAuthCallback) { + clientOptions.options.authCallback = (Auth.TokenParams params) -> { + Object token = ablyLibrary.getRestToken(handle); + if (token != null) return token; + new Handler(Looper.getMainLooper()).post(() -> { + AblyFlutterMessage channelMessage = new AblyFlutterMessage<>(params, handle); + channel.invokeMethod(PlatformConstants.PlatformMethod.authCallback, channelMessage, new MethodChannel.Result() { + @Override + public void success(@Nullable Object result) { + ablyLibrary.setRestToken(handle, result); } - result.success(ablyLibrary.createRest(clientOptions.options)); - } catch (final AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void publishRestMessage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get("channel"); - - final ArrayList channelMessages = (ArrayList) map.get("messages"); - if (channelMessages == null) { - result.error("Messages cannot be null", null, null); - return; - } - Message[] messages = new Message[channelMessages.size()]; - messages = channelMessages.toArray(messages); - ablyLibrary.getRest(messageData.handle).channels.get(channelName).publishAsync(messages, handleCompletionWithListener(result)); - }); - } - - private Callback> paginatedResponseHandler(@NonNull MethodChannel.Result result, Integer handle) { - return new Callback>() { - @Override - public void onSuccess(AsyncPaginatedResult paginatedResult) { - long paginatedResultHandle = _ably.setPaginatedResult(paginatedResult, handle); - result.success(new AblyFlutterMessage<>(paginatedResult, paginatedResultHandle)); - } - - @Override - public void onError(ErrorInfo reason) { - handleAblyException(result, AblyException.fromErrorInfo(reason)); - } - }; - } - - private void getRestHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - ablyLibrary - .getRest(messageData.handle) - .channels.get(channelName) - .historyAsync(params, this.paginatedResponseHandler(result, null)); - }); - } - - private void getRestPresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - ablyLibrary - .getRest(messageData.handle) - .channels.get(channelName) - .presence.getAsync(params, this.paginatedResponseHandler(result, null)); - }); - } - private void getRestPresenceHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - ablyLibrary - .getRest(messageData.handle) - .channels.get(channelName) - .presence.historyAsync(params, this.paginatedResponseHandler(result, null)); - }); - } - - private void getRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - try { - result.success( - Arrays.asList( - ablyLibrary - .getRealtime(messageData.handle) - .channels.get(channelName) - .presence.get(params) - ) - ); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void getRealtimePresenceHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - ablyLibrary - .getRealtime(messageData.handle) - .channels.get(channelName) - .presence.historyAsync(params, this.paginatedResponseHandler(result, null)); - }); - } - - private void enterRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); - final Object data = map.get(PlatformConstants.TxTransportKeys.data); - final Presence presence = ablyLibrary - .getRealtime(messageData.handle) - .channels - .get(channelName) - .presence; - try { - presence.enterClient(clientId, data, handleCompletionWithListener(result)); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void updateRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); - final Object data = map.get(PlatformConstants.TxTransportKeys.data); - final Presence presence = ablyLibrary - .getRealtime(messageData.handle) - .channels - .get(channelName) - .presence; - try { - if (clientId != null) { - presence.updateClient(clientId, data, handleCompletionWithListener(result)); - } else { - presence.update(data, handleCompletionWithListener(result)); + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + System.out.println(errorDetails); + //Do nothing, let another request go to flutter side! } - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - private void leaveRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); - final Object data = map.get(PlatformConstants.TxTransportKeys.data); - final Presence presence = ablyLibrary - .getRealtime(messageData.handle) - .channels - .get(channelName) - .presence; - try { - if (clientId != null) { - presence.leaveClient(clientId, data, handleCompletionWithListener(result)); - } else { - presence.leave(data, handleCompletionWithListener(result)); + @Override + public void notImplemented() { + System.out.println("`authCallback` Method not implemented on dart side"); } - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void createRealtimeWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.ablyDo(message, (ablyLibrary, clientOptions) -> { - try { - final long handle = ablyLibrary.getCurrentHandle(); - if (clientOptions.hasAuthCallback) { - clientOptions.options.authCallback = (Auth.TokenParams params) -> { - Object token = ablyLibrary.getRealtimeToken(handle); - if (token != null) return token; - new Handler(Looper.getMainLooper()).post(() -> { - AblyFlutterMessage channelMessage = new AblyFlutterMessage<>(params, handle); - channel.invokeMethod(PlatformConstants.PlatformMethod.realtimeAuthCallback, channelMessage, new MethodChannel.Result() { - @Override - public void success(@Nullable Object result) { - if (result != null) { - ablyLibrary.setRealtimeToken(handle, result); - } - } - - @Override - public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - System.out.println(errorDetails); - //Do nothing, let another request go to flutter side! - } - - @Override - public void notImplemented() { - System.out.println("`authCallback` Method not implemented on dart side"); - } - }); - }); - return null; - }; - } - result.success(ablyLibrary.createRealtime(clientOptions.options)); - } catch (final AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void connectRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - // Using Number (the superclass of both Long and Integer) because Flutter could send us - // either depending on how big the value is. - // See: https://flutter.dev/docs/development/platform-integration/platform-channels#codec - this.ablyDo(message, (ablyLibrary, realtimeHandle) -> { - ablyLibrary.getRealtime(realtimeHandle.longValue()).connect(); - result.success(null); - }); - } - - private void attachRealtimeChannel( - @NonNull MethodCall call, @NonNull MethodChannel.Result result - ) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { - try { - final String channelName = (String) ablyMessage.message.get("channel"); - final ChannelOptions channelOptions = (ChannelOptions) ablyMessage.message.get("options"); - ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get(channelName, channelOptions) - .attach(handleCompletionWithListener(result)); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void detachRealtimeChannel( - @NonNull MethodCall call, @NonNull MethodChannel.Result result - ) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { - try { - final String channelName = (String) ablyMessage.message.get("channel"); - final ChannelOptions channelOptions = (ChannelOptions) ablyMessage.message.get("options"); - ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get(channelName, channelOptions) - .detach(handleCompletionWithListener(result)); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } + }); + }); - private void setRealtimeChannelOptions( - @NonNull MethodCall call, @NonNull MethodChannel.Result result - ) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { - try { - final String channelName = (String) ablyMessage.message.get("channel"); - final ChannelOptions channelOptions = (ChannelOptions) ablyMessage.message.get("options"); + return null; + }; + } + result.success(ablyLibrary.createRest(clientOptions.options)); + } catch (final AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void setRestChannelOptions( + @NonNull MethodCall call, @NonNull MethodChannel.Result result + ) { + // setOptions is not supported on a rest instance directly + // Track @ https://github.com/ably/ably-flutter/issues/14 + // An alternative is to use the side effect of get channel + // with options which updates passed channel options. + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + final ChannelOptions options = (ChannelOptions) map.get(PlatformConstants.TxTransportKeys.options); + try { + ablyLibrary.getRest(messageData.handle).channels.get(channelName, options); + } catch (AblyException ae) { + handleAblyException(result, ae); + } + }); + } + + private void publishRestMessage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + final ArrayList channelMessages = (ArrayList) map.get(PlatformConstants.TxTransportKeys.messages); + if (channelMessages == null) { + result.error("Messages cannot be null", null, null); + return; + } + Message[] messages = new Message[channelMessages.size()]; + messages = channelMessages.toArray(messages); + ablyLibrary.getRest(messageData.handle).channels.get(channelName).publishAsync(messages, handleCompletionWithListener(result)); + }); + } + + private void releaseRestChannel(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>ablyDo(message, (ablyLibrary, messageData) -> { + final String channelName = messageData.message; + ablyLibrary.getRest(messageData.handle).channels.release(channelName); + result.success(null); + }); + } + + private Callback> paginatedResponseHandler(@NonNull MethodChannel.Result result, Integer handle) { + return new Callback>() { + @Override + public void onSuccess(AsyncPaginatedResult paginatedResult) { + long paginatedResultHandle = _ably.setPaginatedResult(paginatedResult, handle); + result.success(new AblyFlutterMessage<>(paginatedResult, paginatedResultHandle)); + } + + @Override + public void onError(ErrorInfo reason) { + handleAblyException(result, AblyException.fromErrorInfo(reason)); + } + }; + } + + private void getRestHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRest(messageData.handle) + .channels.get(channelName) + .historyAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + + private void getRestPresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRest(messageData.handle) + .channels.get(channelName) + .presence.getAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + + private void getRestPresenceHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRest(messageData.handle) + .channels.get(channelName) + .presence.historyAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + + private void getRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + try { + result.success( + Arrays.asList( ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get(channelName) - .setOptions(channelOptions); - result.success(null); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void publishRealtimeChannelMessage( - @NonNull MethodCall call, @NonNull MethodChannel.Result result - ) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { - try { - final String channelName = (String) ablyMessage.message.get("channel"); - final ChannelOptions channelOptions = (ChannelOptions) ablyMessage.message.get("options"); - final Channel channel = ablyLibrary - .getRealtime(ablyMessage.handle) - .channels - .get(channelName, channelOptions); - - final ArrayList channelMessages = (ArrayList) ablyMessage.message.get("messages"); - if (channelMessages == null) { - result.error("Messages cannot be null", null, null); - return; - } - Message[] messages = new Message[channelMessages.size()]; - messages = channelMessages.toArray(messages); - channel.publish(messages, handleCompletionWithListener(result)); - } catch (AblyException e) { - handleAblyException(result, e); - } - }); - } - - private void getRealtimeHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.>>ablyDo(message, (ablyLibrary, messageData) -> { - final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); - if (params == null) { - params = new Param[0]; - } - ablyLibrary .getRealtime(messageData.handle) .channels.get(channelName) - .historyAsync(params, this.paginatedResponseHandler(result, null)); - }); - } - - private void closeRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.ablyDo(message, (ablyLibrary, realtimeHandle) -> { - ablyLibrary.getRealtime(realtimeHandle.longValue()).close(); - result.success(null); - }); - } - - private void getNextPage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.ablyDo( - message, - (ablyLibrary, pageHandle) -> ablyLibrary - .getPaginatedResult(pageHandle) - .next(this.paginatedResponseHandler(result, pageHandle))); - } - - private void getFirstPage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; - this.ablyDo( - message, - (ablyLibrary, pageHandle) -> ablyLibrary - .getPaginatedResult(pageHandle) - .first(this.paginatedResponseHandler(result, pageHandle))); - } - - void ablyDo(final AblyFlutterMessage message, final BiConsumer consumer) { - consumer.accept(_ably, (Arguments) message.message); - } + .presence.get(params) + ) + ); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void getRealtimePresenceHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRealtime(messageData.handle) + .channels.get(channelName) + .presence.historyAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + + private void enterRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); + final Object data = map.get(PlatformConstants.TxTransportKeys.data); + final Presence presence = ablyLibrary + .getRealtime(messageData.handle) + .channels + .get(channelName) + .presence; + try { + presence.enterClient(clientId, data, handleCompletionWithListener(result)); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void updateRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); + final Object data = map.get(PlatformConstants.TxTransportKeys.data); + final Presence presence = ablyLibrary + .getRealtime(messageData.handle) + .channels + .get(channelName) + .presence; + try { + if (clientId != null) { + presence.updateClient(clientId, data, handleCompletionWithListener(result)); + } else { + presence.update(data, handleCompletionWithListener(result)); + } + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void leaveRealtimePresence(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + final String clientId = (String) map.get(PlatformConstants.TxTransportKeys.clientId); + final Object data = map.get(PlatformConstants.TxTransportKeys.data); + final Presence presence = ablyLibrary + .getRealtime(messageData.handle) + .channels + .get(channelName) + .presence; + try { + if (clientId != null) { + presence.leaveClient(clientId, data, handleCompletionWithListener(result)); + } else { + presence.leave(data, handleCompletionWithListener(result)); + } + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void createRealtimeWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.ablyDo(message, (ablyLibrary, clientOptions) -> { + try { + final long handle = ablyLibrary.getCurrentHandle(); + if (clientOptions.hasAuthCallback) { + clientOptions.options.authCallback = (Auth.TokenParams params) -> { + Object token = ablyLibrary.getRealtimeToken(handle); + if (token != null) return token; + new Handler(Looper.getMainLooper()).post(() -> { + AblyFlutterMessage channelMessage = new AblyFlutterMessage<>(params, handle); + channel.invokeMethod(PlatformConstants.PlatformMethod.realtimeAuthCallback, channelMessage, new MethodChannel.Result() { + @Override + public void success(@Nullable Object result) { + if (result != null) { + ablyLibrary.setRealtimeToken(handle, result); + } + } - private void getPlatformVersion(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - result.success("Android " + android.os.Build.VERSION.RELEASE); - } + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + System.out.println(errorDetails); + //Do nothing, let another request go to flutter side! + } - private void getVersion(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - result.success(Defaults.ABLY_LIB_VERSION); - } -} \ No newline at end of file + @Override + public void notImplemented() { + System.out.println("`authCallback` Method not implemented on dart side"); + } + }); + }); + return null; + }; + } + result.success(ablyLibrary.createRealtime(clientOptions.options)); + } catch (final AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void connectRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + // Using Number (the superclass of both Long and Integer) because Flutter could send us + // either depending on how big the value is. + // See: https://flutter.dev/docs/development/platform-integration/platform-channels#codec + this.ablyDo(message, (ablyLibrary, realtimeHandle) -> { + ablyLibrary.getRealtime(realtimeHandle.longValue()).connect(); + result.success(null); + }); + } + + private void attachRealtimeChannel( + @NonNull MethodCall call, @NonNull MethodChannel.Result result + ) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { + try { + final String channelName = (String) ablyMessage.message.get(PlatformConstants.TxTransportKeys.channelName); + ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get(channelName) + .attach(handleCompletionWithListener(result)); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void detachRealtimeChannel( + @NonNull MethodCall call, @NonNull MethodChannel.Result result + ) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { + try { + final String channelName = (String) ablyMessage.message.get(PlatformConstants.TxTransportKeys.channelName); + ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get(channelName) + .detach(handleCompletionWithListener(result)); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void setRealtimeChannelOptions( + @NonNull MethodCall call, @NonNull MethodChannel.Result result + ) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { + try { + final String channelName = (String) ablyMessage.message.get(PlatformConstants.TxTransportKeys.channelName); + final ChannelOptions channelOptions = (ChannelOptions) ablyMessage.message.get(PlatformConstants.TxTransportKeys.options); + ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get(channelName) + .setOptions(channelOptions); + result.success(null); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void publishRealtimeChannelMessage( + @NonNull MethodCall call, @NonNull MethodChannel.Result result + ) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, ablyMessage) -> { + try { + final String channelName = (String) ablyMessage.message.get(PlatformConstants.TxTransportKeys.channelName); + final Channel channel = ablyLibrary + .getRealtime(ablyMessage.handle) + .channels + .get(channelName); + + final ArrayList channelMessages = (ArrayList) ablyMessage.message.get(PlatformConstants.TxTransportKeys.messages); + if (channelMessages == null) { + result.error("Messages cannot be null", null, null); + return; + } + Message[] messages = new Message[channelMessages.size()]; + messages = channelMessages.toArray(messages); + channel.publish(messages, handleCompletionWithListener(result)); + } catch (AblyException e) { + handleAblyException(result, e); + } + }); + } + + private void releaseRealtimeChannel(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>ablyDo(message, (ablyLibrary, messageData) -> { + final String channelName = messageData.message; + ablyLibrary.getRealtime(messageData.handle).channels.release(channelName); + result.success(null); + }); + } + + private void getRealtimeHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRealtime(messageData.handle) + .channels.get(channelName) + .historyAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + + private void closeRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.ablyDo(message, (ablyLibrary, realtimeHandle) -> { + ablyLibrary.getRealtime(realtimeHandle.longValue()).close(); + result.success(null); + }); + } + + private void getNextPage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.ablyDo( + message, + (ablyLibrary, pageHandle) -> ablyLibrary + .getPaginatedResult(pageHandle) + .next(this.paginatedResponseHandler(result, pageHandle))); + } + + private void getFirstPage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.ablyDo( + message, + (ablyLibrary, pageHandle) -> ablyLibrary + .getPaginatedResult(pageHandle) + .first(this.paginatedResponseHandler(result, pageHandle))); + } + + void ablyDo(final AblyFlutterMessage message, final BiConsumer consumer) { + consumer.accept(_ably, (Arguments) message.message); + } + + private void getPlatformVersion(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + result.success("Android " + android.os.Build.VERSION.RELEASE); + } + + private void getVersion(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + result.success(Defaults.ABLY_LIB_VERSION); + } +} diff --git a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java index c603eee60..2d2887a78 100644 --- a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java +++ b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java @@ -17,15 +17,17 @@ static final public class CodecTypes { public static final byte tokenParams = (byte) 133; public static final byte tokenDetails = (byte) 134; public static final byte tokenRequest = (byte) 135; - public static final byte paginatedResult = (byte) 136; - public static final byte restHistoryParams = (byte) 137; - public static final byte realtimeHistoryParams = (byte) 138; - public static final byte restPresenceParams = (byte) 139; - public static final byte presenceMessage = (byte) 140; - public static final byte realtimePresenceParams = (byte) 141; - public static final byte errorInfo = (byte) 142; - public static final byte connectionStateChange = (byte) 143; - public static final byte channelStateChange = (byte) 144; + public static final byte restChannelOptions = (byte) 136; + public static final byte realtimeChannelOptions = (byte) 137; + public static final byte paginatedResult = (byte) 138; + public static final byte restHistoryParams = (byte) 139; + public static final byte realtimeHistoryParams = (byte) 140; + public static final byte restPresenceParams = (byte) 141; + public static final byte presenceMessage = (byte) 142; + public static final byte realtimePresenceParams = (byte) 143; + public static final byte errorInfo = (byte) 144; + public static final byte connectionStateChange = (byte) 145; + public static final byte channelStateChange = (byte) 146; } static final public class PlatformMethod { @@ -35,10 +37,12 @@ static final public class PlatformMethod { public static final String authCallback = "authCallback"; public static final String realtimeAuthCallback = "realtimeAuthCallback"; public static final String createRestWithOptions = "createRestWithOptions"; + public static final String setRestChannelOptions = "setRestChannelOptions"; public static final String publish = "publish"; public static final String restHistory = "restHistory"; public static final String restPresenceGet = "restPresenceGet"; public static final String restPresenceHistory = "restPresenceHistory"; + public static final String releaseRestChannel = "releaseRestChannel"; public static final String createRealtimeWithOptions = "createRealtimeWithOptions"; public static final String connectRealtime = "connectRealtime"; public static final String closeRealtime = "closeRealtime"; @@ -52,6 +56,7 @@ static final public class PlatformMethod { public static final String realtimePresenceLeave = "realtimePresenceLeave"; public static final String onRealtimePresenceMessage = "onRealtimePresenceMessage"; public static final String publishRealtimeChannelMessage = "publishRealtimeChannelMessage"; + public static final String releaseRealtimeChannel = "releaseRealtimeChannel"; public static final String realtimeHistory = "realtimeHistory"; public static final String onRealtimeConnectionStateChanged = "onRealtimeConnectionStateChanged"; public static final String onRealtimeChannelStateChanged = "onRealtimeChannelStateChanged"; @@ -66,6 +71,7 @@ static final public class TxTransportKeys { public static final String data = "data"; public static final String clientId = "clientId"; public static final String options = "options"; + public static final String messages = "messages"; } static final public class TxAblyMessage { @@ -130,6 +136,16 @@ static final public class TxClientOptions { public static final String transportParams = "transportParams"; } + static final public class TxRestChannelOptions { + public static final String cipher = "cipher"; + } + + static final public class TxRealtimeChannelOptions { + public static final String cipher = "cipher"; + public static final String params = "params"; + public static final String modes = "modes"; + } + static final public class TxTokenDetails { public static final String token = "token"; public static final String expires = "expires"; @@ -174,6 +190,10 @@ static final public class TxEnumConstants { public static final String enter = "enter"; public static final String present = "present"; public static final String update = "update"; + public static final String presence = "presence"; + public static final String publish = "publish"; + public static final String subscribe = "subscribe"; + public static final String presenceSubscribe = "presenceSubscribe"; } static final public class TxConnectionStateChange { diff --git a/bin/codegencontext.dart b/bin/codegencontext.dart index 9fd169813..2f2b645fb 100644 --- a/bin/codegencontext.dart +++ b/bin/codegencontext.dart @@ -12,6 +12,8 @@ Iterable> get _types sync* { 'tokenParams', 'tokenDetails', 'tokenRequest', + 'restChannelOptions', + 'realtimeChannelOptions', 'paginatedResult', 'restHistoryParams', 'realtimeHistoryParams', @@ -53,10 +55,12 @@ const List> _platformMethods = [ // Rest {'name': 'createRestWithOptions', 'value': 'createRestWithOptions'}, + {'name': 'setRestChannelOptions', 'value': 'setRestChannelOptions'}, {'name': 'publish', 'value': 'publish'}, {'name': 'restHistory', 'value': 'restHistory'}, {'name': 'restPresenceGet', 'value': 'restPresenceGet'}, {'name': 'restPresenceHistory', 'value': 'restPresenceHistory'}, + {'name': 'releaseRestChannel', 'value': 'releaseRestChannel'}, // Realtime {'name': 'createRealtimeWithOptions', 'value': 'createRealtimeWithOptions'}, @@ -75,6 +79,7 @@ const List> _platformMethods = [ 'name': 'publishRealtimeChannelMessage', 'value': 'publishRealtimeChannelMessage' }, + {'name': 'releaseRealtimeChannel', 'value': 'releaseRealtimeChannel'}, {'name': 'realtimeHistory', 'value': 'realtimeHistory'}, // Realtime events @@ -102,6 +107,7 @@ const List> _objects = [ 'data', 'clientId', 'options', + 'messages', ] }, { @@ -167,6 +173,18 @@ const List> _objects = [ 'transportParams', ] }, + { + 'name': 'RestChannelOptions', + 'properties': ['cipher'] + }, + { + 'name': 'RealtimeChannelOptions', + 'properties': [ + 'cipher', + 'params', + 'modes', + ] + }, { 'name': 'TokenDetails', 'properties': [ @@ -202,6 +220,7 @@ const List> _objects = [ { 'name': 'EnumConstants', 'properties': [ + // connection & channel - states & events 'initialized', 'connecting', 'connected', @@ -214,11 +233,17 @@ const List> _objects = [ 'closing', 'closed', 'failed', + // Presence actions 'absent', 'leave', 'enter', 'present', 'update', + // channel modes + 'presence', + 'publish', + 'subscribe', + 'presenceSubscribe', ] }, { diff --git a/bin/templates/platformconstants.h.dart b/bin/templates/platformconstants.h.dart index 0bf0b45e6..4262ede8f 100644 --- a/bin/templates/platformconstants.h.dart +++ b/bin/templates/platformconstants.h.dart @@ -6,12 +6,10 @@ typedef NS_ENUM(UInt8, _Value) { }; -@interface AblyPlatformMethod : NSObject +// flutter platform channel method names ${c['methods'].map((_) => 'extern NSString *const AblyPlatformMethod_${_['name']};').join('\n')} -@end ${c['objects'].map((_) => ''' -@interface Tx${_['name']} : NSObject +// key constants for ${_['name']} ${_['properties'].map((name) => 'extern NSString *const Tx${_['name']}_$name;').join('\n')} -@end ''').join('\n')}'''; diff --git a/bin/templates/platformconstants.m.dart b/bin/templates/platformconstants.m.dart index 7263a058f..9ac7657b7 100644 --- a/bin/templates/platformconstants.m.dart +++ b/bin/templates/platformconstants.m.dart @@ -2,12 +2,10 @@ String $(Map c) => ''' #import "AblyPlatformConstants.h" -@implementation AblyPlatformMethod +// flutter platform channel method names ${c['methods'].map((_) => 'NSString *const AblyPlatformMethod_${_['name']}= @"${_['value']}";').join('\n')} -@end ${c['objects'].map((_) => ''' -@implementation Tx${_['name']} +// key constants for ${_['name']} ${_['properties'].map((name) => 'NSString *const Tx${_['name']}_$name = @"$name";').join('\n')} -@end ''').join('\n')}'''; diff --git a/example/android/app/src/main/java/io/ably/flutter/example/MainActivity.java b/example/android/app/src/main/java/io/ably/flutter/example/MainActivity.java new file mode 100644 index 000000000..0d4cde7a0 --- /dev/null +++ b/example/android/app/src/main/java/io/ably/flutter/example/MainActivity.java @@ -0,0 +1,6 @@ +package io.ably.flutter.example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index be90efaa2..7afcfe760 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - Ably (1.2.3): + - Ably (1.2.4): - AblyDeltaCodec (= 1.2.0) - msgpack (= 0.3.1) - - SocketRocketAblyFork (= 0.5.2-ably-7) + - SocketRocketAblyFork (= 0.5.2-ably-8) - ULID (= 1.1.0) - ably_flutter (0.0.5): - - Ably + - Ably (= 1.2.4) - Flutter - AblyDeltaCodec (1.2.0) - Flutter (1.0.0) - msgpack (0.3.1) - - SocketRocketAblyFork (0.5.2-ably-7) + - SocketRocketAblyFork (0.5.2-ably-8) - ULID (1.1.0) DEPENDENCIES: @@ -32,14 +32,14 @@ EXTERNAL SOURCES: :path: Flutter SPEC CHECKSUMS: - Ably: 9f143677993cb6df20fce4551b9b6b2e55ad2f4d - ably_flutter: a35c3926509e76b0fc69b9199872b9d428882611 + Ably: 863d35bb6a6aa5fc537304ef19f85cfa51d1bbde + ably_flutter: b37439c7b63f4b3e44f4674c5bfb56452d193ad0 AblyDeltaCodec: 6123f31df5b04a0f5452968505a46ba16a9eb689 - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c msgpack: a14de9216d29cfd0a7aff5af5150601a27e899a4 - SocketRocketAblyFork: 33ff506ecb565498051b8f358d60ae44274c3981 + SocketRocketAblyFork: e6e3d820bf8ff9e6265d2af81b9f11f270351ea6 ULID: b4714891a02819364faecd574a53e391c4c6de9d PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index bc6bae0f4..0038c0985 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -231,7 +231,6 @@ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Ably/Ably.framework", "${BUILT_PRODUCTS_DIR}/AblyDeltaCodec/AblyDeltaCodec.framework", - "${PODS_ROOT}/../Flutter/Flutter.framework", "${BUILT_PRODUCTS_DIR}/SocketRocketAblyFork/SocketRocketAblyFork.framework", "${BUILT_PRODUCTS_DIR}/ULID/ULID.framework", "${BUILT_PRODUCTS_DIR}/ably_flutter/ably_flutter.framework", @@ -241,7 +240,6 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ably.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AblyDeltaCodec.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocketAblyFork.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ULID.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ably_flutter.framework", diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 776f1ee3c..8f905e65e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:ably_flutter/ably_flutter.dart' as ably; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'provisioning.dart' as provisioning; @@ -100,12 +99,12 @@ class _MyAppState extends State { // Platform messages may fail, so we use a try/catch PlatformException. try { platformVersion = await ably.platformVersion(); - } on PlatformException { + } on ably.AblyException { platformVersion = 'Failed to get platform version.'; } try { ablyVersion = await ably.version(); - } on PlatformException { + } on ably.AblyException { ablyVersion = 'Failed to get Ably version.'; } @@ -558,6 +557,14 @@ class _MyAppState extends State { }); }); + Widget releaseRestChannel() => FlatButton( + color: Colors.deepOrangeAccent[100], + onPressed: (_rest == null) + ? null + : () => _rest.channels.release(defaultChannel), + child: const Text('Release Rest channel'), + ); + Widget getRealtimeChannelHistory() => getPageNavigator( name: 'Realtime history', enabled: _realtime != null, @@ -637,6 +644,14 @@ class _MyAppState extends State { }); }); + Widget releaseRealtimeChannel() => FlatButton( + color: Colors.deepOrangeAccent[100], + onPressed: (_realtime == null) + ? null + : () => _realtime.channels.release(defaultChannel), + child: const Text('Release Realtime channel'), + ); + final List _presenceData = [ null, 1, @@ -810,6 +825,7 @@ class _MyAppState extends State { ?.map((m) => Text('${m.id}:${m.clientId}:${m.data}')) ?.toList() ?? [], + releaseRealtimeChannel(), const Divider(), const Text( 'Rest', @@ -838,6 +854,7 @@ class _MyAppState extends State { ?.map((m) => Text('${m.id}:${m.clientId}:${m.data}')) ?.toList() ?? [], + releaseRestChannel(), ]), ), ), diff --git a/example/lib/provisioning.dart b/example/lib/provisioning.dart index 8cfec525c..c4e0d92f3 100644 --- a/example/lib/provisioning.dart +++ b/example/lib/provisioning.dart @@ -13,7 +13,7 @@ String tokenDetailsURL(String keyName, [String prefix = '']) => 'https://${prefix}rest.ably.io/keys/$keyName/requestToken'; // per: https://docs.ably.io/client-lib-development-guide/test-api/ -final _appSpec = Map.unmodifiable({ +final _appSpec = Map.unmodifiable({ // API Keys & Capabilities. 'keys': [ { @@ -42,22 +42,34 @@ class AppKey { String toString() => _keyStr; } -Future _provisionApp(final String environmentPrefix) async { +Future _provisionApp( + final String environmentPrefix, [ + Map appSpec, +]) async { + appSpec ??= _appSpec; final url = 'https://${environmentPrefix}rest.ably.io/apps'; - final body = jsonEncode(_appSpec); - final response = await http.post(url, body: body, headers: _requestHeaders); + final body = jsonEncode(appSpec); + final response = await http.post( + Uri.parse(url), + body: body, + headers: _requestHeaders, + ); if (response.statusCode != HttpStatus.created) { + print("Server didn't return success. ${response.body}"); throw HttpException("Server didn't return success." - ' Status: ${response.statusCode}'); + ' Status: ${response.statusCode} : ${response.body}'); } return jsonDecode(response.body) as Map; } -Future provision(String environmentPrefix) async { +Future provision( + String environmentPrefix, [ + Map appSpec, +]) async { final result = await const RetryOptions( maxAttempts: 5, delayFactor: Duration(seconds: 2), - ).retry(() => _provisionApp(environmentPrefix)); + ).retry(() => _provisionApp(environmentPrefix, appSpec)); final key = result['keys'][0]; return AppKey( key['keyName'] as String, @@ -72,7 +84,7 @@ Future> getTokenRequest() async { final r = await const RetryOptions( maxAttempts: 5, delayFactor: Duration(seconds: 2), - ).retry(() => http.get(authURL)); + ).retry(() => http.get(Uri.parse(authURL))); print('tokenRequest from tokenRequest server: ${r.body}'); return Map.castFrom( jsonDecode(r.body) as Map, @@ -90,7 +102,7 @@ Future> getTokenDetails( maxAttempts: 5, delayFactor: Duration(seconds: 2), ).retry(() => http.post( - tokenDetailsURL(keyName, prefix), + Uri.parse(tokenDetailsURL(keyName, prefix)), headers: { 'Authorization': 'Basic $encoded', 'Content-Type': 'application/json', diff --git a/example/pubspec.lock b/example/pubspec.lock index eadc16b21..4df7aca64 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,14 +7,14 @@ packages: path: ".." relative: true source: path - version: "1.2.0-preview.1" + version: "1.2.0-preview.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" boolean_selector: dependency: transitive description: @@ -73,14 +73,14 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.13.3" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "4.0.0" matcher: dependency: transitive description: @@ -108,7 +108,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.2" + version: "1.11.0" retry: dependency: "direct main" description: @@ -127,7 +127,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -162,7 +162,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.3.0" typed_data: dependency: transitive description: @@ -178,5 +178,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=2.12.0-0.0 <3.0.0" + dart: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d47cfd3c2..4ca4378da 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - http: ^0.12.0+4 + http: ^0.13.3 retry: ^3.0.1 dev_dependencies: diff --git a/ios/Classes/AblyFlutterPlugin.m b/ios/Classes/AblyFlutterPlugin.m index 69364f2ed..51f5ee8a0 100644 --- a/ios/Classes/AblyFlutterPlugin.m +++ b/ios/Classes/AblyFlutterPlugin.m @@ -46,13 +46,28 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; result([ably createRestWithOptions:message.message]); }; +static const FlutterHandler _setRestChannelOptions = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { + AblyFlutterMessage *const message = call.arguments; + AblyFlutter *const ably = [plugin ably]; + AblyFlutterMessage *const messageData = message.message; + + NSMutableDictionary *const _dataMap = messageData.message; + NSString *const channelName = (NSString*)[_dataMap objectForKey:TxTransportKeys_channelName]; + ARTChannelOptions *const channelOptions = (ARTChannelOptions*)[_dataMap objectForKey:TxTransportKeys_options]; + + ARTRest *const client = [ably getRest:messageData.handle]; + ARTRestChannel *const channel = [client.channels get:channelName]; + [channel setOptions:channelOptions]; + result(nil); +}; + static const FlutterHandler _publishRestMessage = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; AblyFlutter *const ably = [plugin ably]; AblyFlutterMessage *const messageData = message.message; NSMutableDictionary *const _dataMap = messageData.message; - NSString *const channelName = (NSString*)[_dataMap objectForKey:@"channel"]; - NSArray *const messages = (NSArray*)[_dataMap objectForKey:@"messages"]; + NSString *const channelName = (NSString*)[_dataMap objectForKey:TxTransportKeys_channelName]; + NSArray *const messages = (NSArray*)[_dataMap objectForKey:TxTransportKeys_messages]; ARTRest *const client = [ably getRest:messageData.handle]; ARTRestChannel *const channel = [client.channels get:channelName]; @@ -80,7 +95,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTDataQuery *const dataQuery = (ARTDataQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRest *const client = [ably getRest:messageData.handle]; ARTRestChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { if(error){ result([ FlutterError @@ -94,9 +109,9 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [channel history:dataQuery callback:cbk error: nil]; + [channel history:dataQuery callback:callback error: nil]; } else { - [channel history:cbk]; + [channel history:callback]; } }; @@ -109,7 +124,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTPresenceQuery *const dataQuery = (ARTPresenceQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRest *const client = [ably getRest:messageData.handle]; ARTRestChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { if(error){ result([ FlutterError @@ -123,9 +138,9 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [[channel presence] get:dataQuery callback:cbk error:nil]; + [[channel presence] get:dataQuery callback:callback error:nil]; } else { - [[channel presence] get:cbk]; + [[channel presence] get:callback]; } }; @@ -138,7 +153,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTPresenceQuery *const dataQuery = (ARTPresenceQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRest *const client = [ably getRest:messageData.handle]; ARTRestChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { if(error){ result([ FlutterError @@ -152,12 +167,22 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [[channel presence] history:dataQuery callback:cbk error:nil]; + [[channel presence] history:dataQuery callback:callback error:nil]; } else { - [[channel presence] history:cbk]; + [[channel presence] history:callback]; } }; +static const FlutterHandler _releaseRestChannel = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { + AblyFlutterMessage *const message = call.arguments; + AblyFlutter *const ably = [plugin ably]; + AblyFlutterMessage *const messageData = message.message; + ARTRest *const client = [ably getRest:messageData.handle]; + NSString *const channelName = (NSString*)messageData.message; + [client.channels release:channelName]; + result(nil); +}; + static const FlutterHandler _createRealtimeWithOptions = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; AblyFlutter *const ably = [plugin ably]; @@ -188,9 +213,8 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtime *const realtimeWithHandle = [ably realtimeWithHandle: realtimeHandle]; NSDictionary *const realtimePayload = data.message; - NSString *const channelName = (NSString*)[realtimePayload objectForKey:@"channel"]; - ARTChannelOptions *const channelOptions = (ARTChannelOptions*)[realtimePayload objectForKey:@"options"]; - ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + NSString *const channelName = (NSString*)[realtimePayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName]; [channel attach:^(ARTErrorInfo *_Nullable error){ if (error) { result([ @@ -213,9 +237,8 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtime *const realtimeWithHandle = [ably realtimeWithHandle: realtimeHandle]; NSDictionary *const realtimePayload = data.message; - NSString *const channelName = (NSString*)[realtimePayload objectForKey:@"channel"]; - ARTChannelOptions *const channelOptions = (ARTChannelOptions*)[realtimePayload objectForKey:@"options"]; - ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + NSString *const channelName = (NSString*)[realtimePayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName]; [channel detach:^(ARTErrorInfo *_Nullable error){ if (error) { result([ @@ -238,9 +261,8 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtime *const realtimeWithHandle = [ably realtimeWithHandle: realtimeHandle]; NSDictionary *const realtimePayload = data.message; - NSString *const channelName = (NSString*)[realtimePayload objectForKey:@"channel"]; - ARTChannelOptions *const channelOptions = (ARTChannelOptions*)[realtimePayload objectForKey:@"options"]; - ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + NSString *const channelName = (NSString*)[realtimePayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName]; void (^callback)(ARTErrorInfo *_Nullable) = ^(ARTErrorInfo *_Nullable error){ if (error) { result( @@ -253,12 +275,36 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; - NSArray *const messages = (NSArray*)[realtimePayload objectForKey:@"messages"]; + NSArray *const messages = (NSArray*)[realtimePayload objectForKey:TxTransportKeys_messages]; [channel publish:messages callback:callback]; }; static const FlutterHandler _setRealtimeChannelOptions = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { - // cocoa library doesn't support setOptions yet! + AblyFlutterMessage *const message = call.arguments; + AblyFlutter *const ably = [plugin ably]; + AblyFlutterMessage *const data = message.message; + NSNumber *const realtimeHandle = data.handle; + ARTRealtime *const realtimeWithHandle = [ably realtimeWithHandle: realtimeHandle]; + + NSDictionary *const realtimePayload = data.message; + NSString *const channelName = (NSString*)[realtimePayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannelOptions *const channelOptions = (ARTRealtimeChannelOptions*)[realtimePayload objectForKey:TxTransportKeys_options]; + + ARTRealtimeChannel *const channel = [realtimeWithHandle.channels get:channelName]; + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + if (error) { + result([ + FlutterError + errorWithCode:[NSString stringWithFormat: @"%ld", (long)error.code] + message:[NSString stringWithFormat:@"Error getting realtime channel history; err = %@", [error message]] + details:error + ]); + } else { + NSNumber *const paginatedResultHandle = [ably setPaginatedResult:paginatedResult handle:nil]; + result([[AblyFlutterMessage alloc] initWithMessage:paginatedResult handle: paginatedResultHandle]); + } + }; + [channel setOptions:channelOptions callback:callback]; result(nil); }; @@ -271,7 +317,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtimeHistoryQuery *const dataQuery = (ARTRealtimeHistoryQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRealtime *const client = [ably realtimeWithHandle:messageData.handle]; ARTRealtimeChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { if (error) { result([ FlutterError @@ -285,9 +331,9 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [channel history:dataQuery callback:cbk error: nil]; + [channel history:dataQuery callback:callback error: nil]; } else { - [channel history:cbk]; + [channel history:callback]; } }; @@ -300,7 +346,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtimePresenceQuery *const dataQuery = (ARTRealtimePresenceQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRealtime *const client = [ably realtimeWithHandle:messageData.handle]; ARTRealtimeChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(NSArray * _Nullable presenceMembers, ARTErrorInfo * _Nullable error) { + const id callback = ^(NSArray * _Nullable presenceMembers, ARTErrorInfo * _Nullable error) { if (error) { result([ FlutterError @@ -313,9 +359,9 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [[channel presence] get:dataQuery callback:cbk]; + [[channel presence] get:dataQuery callback:callback]; } else { - [[channel presence] get:cbk]; + [[channel presence] get:callback]; } }; @@ -328,7 +374,7 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; ARTRealtimeHistoryQuery *const dataQuery = (ARTRealtimeHistoryQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRealtime *const client = [ably realtimeWithHandle:messageData.handle]; ARTRealtimeChannel *const channel = [client.channels get:channelName]; - const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + const id callback = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { if (error) { result([ FlutterError @@ -342,9 +388,9 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; } }; if (dataQuery) { - [[channel presence] history:dataQuery callback:cbk error:nil]; + [[channel presence] history:dataQuery callback:callback error:nil]; } else { - [[channel presence] history:cbk]; + [[channel presence] history:callback]; } }; @@ -420,6 +466,16 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; }]; }; +static const FlutterHandler _releaseRealtimeChannel = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { + AblyFlutterMessage *const message = call.arguments; + AblyFlutter *const ably = [plugin ably]; + AblyFlutterMessage *const messageData = message.message; + ARTRealtime *const client = [ably realtimeWithHandle:messageData.handle]; + NSString *const channelName = (NSString*)messageData.message; + [client.channels release:channelName]; + result(nil); +}; + static const FlutterHandler _getNextPage = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; AblyFlutter *const ably = [plugin ably]; @@ -511,16 +567,18 @@ -(instancetype)initWithChannel:(FlutterMethodChannel *const)channel streamsChann AblyPlatformMethod_getVersion: _getVersion, AblyPlatformMethod_registerAbly: _register, AblyPlatformMethod_createRestWithOptions: _createRestWithOptions, + AblyPlatformMethod_setRestChannelOptions: _setRestChannelOptions, AblyPlatformMethod_publish: _publishRestMessage, AblyPlatformMethod_restHistory: _getRestHistory, AblyPlatformMethod_restPresenceGet: _getRestPresence, AblyPlatformMethod_restPresenceHistory: _getRestPresenceHistory, + AblyPlatformMethod_releaseRestChannel: _releaseRestChannel, AblyPlatformMethod_createRealtimeWithOptions: _createRealtimeWithOptions, + AblyPlatformMethod_setRealtimeChannelOptions: _setRealtimeChannelOptions, AblyPlatformMethod_connectRealtime: _connectRealtime, AblyPlatformMethod_closeRealtime: _closeRealtime, AblyPlatformMethod_attachRealtimeChannel: _attachRealtimeChannel, AblyPlatformMethod_detachRealtimeChannel: _detachRealtimeChannel, - AblyPlatformMethod_setRealtimeChannelOptions: _setRealtimeChannelOptions, AblyPlatformMethod_publishRealtimeChannelMessage: _publishRealtimeChannelMessage, AblyPlatformMethod_realtimeHistory: _getRealtimeHistory, AblyPlatformMethod_nextPage: _getNextPage, @@ -530,6 +588,7 @@ -(instancetype)initWithChannel:(FlutterMethodChannel *const)channel streamsChann AblyPlatformMethod_realtimePresenceEnter: _enterRealtimePresence, AblyPlatformMethod_realtimePresenceUpdate: _updateRealtimePresence, AblyPlatformMethod_realtimePresenceLeave: _leaveRealtimePresence, + AblyPlatformMethod_releaseRealtimeChannel: _releaseRealtimeChannel, }; _nextRegistration = 1; diff --git a/ios/Classes/AblyFlutterStreamHandler.m b/ios/Classes/AblyFlutterStreamHandler.m index d44504762..013552a0f 100644 --- a/ios/Classes/AblyFlutterStreamHandler.m +++ b/ios/Classes/AblyFlutterStreamHandler.m @@ -42,9 +42,8 @@ - (void) startListening:(AblyFlutterMessage *const)message emitter:(FlutterEvent NSMutableDictionary* eventPayload = eventMessage.message; ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; - NSString *channelName = (NSString*)[eventPayload objectForKey:@"channel"]; - ARTRealtimeChannelOptions *channelOptions = (ARTRealtimeChannelOptions*)[eventPayload objectForKey:@"options"]; - ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + NSString *channelName = (NSString*)[eventPayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName]; listener = [channel on: ^(ARTChannelStateChange * const stateChange) { emitter(stateChange); @@ -54,9 +53,8 @@ - (void) startListening:(AblyFlutterMessage *const)message emitter:(FlutterEvent NSMutableDictionary* eventPayload = eventMessage.message; ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; - NSString *channelName = (NSString*)[eventPayload objectForKey:@"channel"]; - ARTRealtimeChannelOptions *channelOptions = (ARTRealtimeChannelOptions*)[eventPayload objectForKey:@"options"]; - ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + NSString *channelName = (NSString*)[eventPayload objectForKey:TxTransportKeys_channelName]; + ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName]; listener = [channel subscribe: ^(ARTMessage * const message) { emitter(message); @@ -67,8 +65,7 @@ - (void) startListening:(AblyFlutterMessage *const)message emitter:(FlutterEvent ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; NSString *channelName = (NSString*)[eventPayload objectForKey:TxTransportKeys_channelName]; - ARTRealtimeChannelOptions *channelOptions = (ARTRealtimeChannelOptions*)[eventPayload objectForKey:TxTransportKeys_options]; - ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName options:channelOptions]; + ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName]; listener = [[channel presence] subscribe: ^(ARTPresenceMessage * const message) { emitter(message); }]; @@ -94,7 +91,7 @@ - (void) cancelListening:(AblyFlutterMessage *const)message { NSMutableDictionary* eventPayload = eventMessage.message; ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; - NSString *channelName = (NSString*)[eventPayload objectForKey:@"channel"]; + NSString *channelName = (NSString*)[eventPayload objectForKey:TxTransportKeys_channelName]; ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName]; [channel off: listener]; } else if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) { @@ -102,7 +99,7 @@ - (void) cancelListening:(AblyFlutterMessage *const)message { NSMutableDictionary* eventPayload = eventMessage.message; ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; - NSString *channelName = (NSString*)[eventPayload objectForKey:@"channel"]; + NSString *channelName = (NSString*)[eventPayload objectForKey:TxTransportKeys_channelName]; ARTRealtimeChannel *channel = [realtimeWithHandle.channels get:channelName]; [channel unsubscribe: listener]; } else if ([AblyPlatformMethod_onRealtimePresenceMessage isEqual: eventName]) { diff --git a/ios/Classes/codec/AblyFlutterReader.m b/ios/Classes/codec/AblyFlutterReader.m index 85203d21a..410caa8cd 100644 --- a/ios/Classes/codec/AblyFlutterReader.m +++ b/ios/Classes/codec/AblyFlutterReader.m @@ -33,6 +33,8 @@ + (AblyCodecDecoder) getDecoder:(const NSString*)type { [NSString stringWithFormat:@"%d", messageCodecType]: readChannelMessage, [NSString stringWithFormat:@"%d", tokenDetailsCodecType]: readTokenDetails, [NSString stringWithFormat:@"%d", tokenRequestCodecType]: readTokenRequest, + [NSString stringWithFormat:@"%d", restChannelOptionsCodecType]: readRestChannelOptions, + [NSString stringWithFormat:@"%d", realtimeChannelOptionsCodecType]: readRealtimeChannelOptions, [NSString stringWithFormat:@"%d", restHistoryParamsCodecType]: readRestHistoryParams, [NSString stringWithFormat:@"%d", realtimeHistoryParamsCodecType]: readRealtimeHistoryParams, [NSString stringWithFormat:@"%d", restPresenceParamsCodecType]: readRestPresenceParams, @@ -185,6 +187,36 @@ +(ARTTokenParams *)tokenParamsFromDictionary: (NSDictionary *) dictionary { return o; } +static AblyCodecDecoder readRestChannelOptions = ^ARTChannelOptions*(NSDictionary *const dictionary) { + ARTChannelOptions *const o = [ARTChannelOptions new]; + READ_VALUE(o, cipher, dictionary, TxRealtimeChannelOptions_cipher); + return o; +}; + +static AblyCodecDecoder readRealtimeChannelOptions = ^ARTRealtimeChannelOptions*(NSDictionary *const dictionary) { + ARTRealtimeChannelOptions *const o = [ARTRealtimeChannelOptions new]; + READ_VALUE(o, cipher, dictionary, TxRealtimeChannelOptions_cipher); + READ_VALUE(o, params, dictionary, TxRealtimeChannelOptions_params); + ON_VALUE(^(const id value) { + NSArray* modes = (NSArray *)value; + ARTChannelMode options = 0; + if ([modes containsObject:TxEnumConstants_presence]) { + options = options | ARTChannelModePresence; + } + if ([modes containsObject:TxEnumConstants_subscribe]) { + options = options | ARTChannelModeSubscribe; + } + if ([modes containsObject:TxEnumConstants_publish]) { + options = options | ARTChannelModePublish; + } + if ([modes containsObject:TxEnumConstants_presenceSubscribe]) { + options = options | ARTChannelModePresenceSubscribe; + } + o.modes = options; + }, dictionary, TxRealtimeChannelOptions_modes); + return o; +}; + static AblyCodecDecoder readChannelMessage = ^ARTMessage*(NSDictionary *const dictionary) { ARTMessage *const o = [ARTMessage new]; READ_VALUE(o, id, dictionary, TxMessage_id); diff --git a/ios/Classes/codec/AblyPlatformConstants.h b/ios/Classes/codec/AblyPlatformConstants.h index b33b7edd6..215ec7752 100644 --- a/ios/Classes/codec/AblyPlatformConstants.h +++ b/ios/Classes/codec/AblyPlatformConstants.h @@ -14,29 +14,33 @@ typedef NS_ENUM(UInt8, _Value) { tokenParamsCodecType = 133, tokenDetailsCodecType = 134, tokenRequestCodecType = 135, - paginatedResultCodecType = 136, - restHistoryParamsCodecType = 137, - realtimeHistoryParamsCodecType = 138, - restPresenceParamsCodecType = 139, - presenceMessageCodecType = 140, - realtimePresenceParamsCodecType = 141, - errorInfoCodecType = 142, - connectionStateChangeCodecType = 143, - channelStateChangeCodecType = 144, + restChannelOptionsCodecType = 136, + realtimeChannelOptionsCodecType = 137, + paginatedResultCodecType = 138, + restHistoryParamsCodecType = 139, + realtimeHistoryParamsCodecType = 140, + restPresenceParamsCodecType = 141, + presenceMessageCodecType = 142, + realtimePresenceParamsCodecType = 143, + errorInfoCodecType = 144, + connectionStateChangeCodecType = 145, + channelStateChangeCodecType = 146, }; -@interface AblyPlatformMethod : NSObject +// flutter platform channel method names extern NSString *const AblyPlatformMethod_getPlatformVersion; extern NSString *const AblyPlatformMethod_getVersion; extern NSString *const AblyPlatformMethod_registerAbly; extern NSString *const AblyPlatformMethod_authCallback; extern NSString *const AblyPlatformMethod_realtimeAuthCallback; extern NSString *const AblyPlatformMethod_createRestWithOptions; +extern NSString *const AblyPlatformMethod_setRestChannelOptions; extern NSString *const AblyPlatformMethod_publish; extern NSString *const AblyPlatformMethod_restHistory; extern NSString *const AblyPlatformMethod_restPresenceGet; extern NSString *const AblyPlatformMethod_restPresenceHistory; +extern NSString *const AblyPlatformMethod_releaseRestChannel; extern NSString *const AblyPlatformMethod_createRealtimeWithOptions; extern NSString *const AblyPlatformMethod_connectRealtime; extern NSString *const AblyPlatformMethod_closeRealtime; @@ -50,49 +54,45 @@ extern NSString *const AblyPlatformMethod_realtimePresenceUpdate; extern NSString *const AblyPlatformMethod_realtimePresenceLeave; extern NSString *const AblyPlatformMethod_onRealtimePresenceMessage; extern NSString *const AblyPlatformMethod_publishRealtimeChannelMessage; +extern NSString *const AblyPlatformMethod_releaseRealtimeChannel; extern NSString *const AblyPlatformMethod_realtimeHistory; extern NSString *const AblyPlatformMethod_onRealtimeConnectionStateChanged; extern NSString *const AblyPlatformMethod_onRealtimeChannelStateChanged; extern NSString *const AblyPlatformMethod_onRealtimeChannelMessage; extern NSString *const AblyPlatformMethod_nextPage; extern NSString *const AblyPlatformMethod_firstPage; -@end -@interface TxTransportKeys : NSObject +// key constants for TransportKeys extern NSString *const TxTransportKeys_channelName; extern NSString *const TxTransportKeys_params; extern NSString *const TxTransportKeys_data; extern NSString *const TxTransportKeys_clientId; extern NSString *const TxTransportKeys_options; -@end +extern NSString *const TxTransportKeys_messages; -@interface TxAblyMessage : NSObject +// key constants for AblyMessage extern NSString *const TxAblyMessage_registrationHandle; extern NSString *const TxAblyMessage_type; extern NSString *const TxAblyMessage_message; -@end -@interface TxAblyEventMessage : NSObject +// key constants for AblyEventMessage extern NSString *const TxAblyEventMessage_eventName; extern NSString *const TxAblyEventMessage_type; extern NSString *const TxAblyEventMessage_message; -@end -@interface TxErrorInfo : NSObject +// key constants for ErrorInfo extern NSString *const TxErrorInfo_code; extern NSString *const TxErrorInfo_message; extern NSString *const TxErrorInfo_statusCode; extern NSString *const TxErrorInfo_href; extern NSString *const TxErrorInfo_requestId; extern NSString *const TxErrorInfo_cause; -@end -@interface TxMessageData : NSObject +// key constants for MessageData extern NSString *const TxMessageData_data; extern NSString *const TxMessageData_type; -@end -@interface TxClientOptions : NSObject +// key constants for ClientOptions extern NSString *const TxClientOptions_authUrl; extern NSString *const TxClientOptions_authMethod; extern NSString *const TxClientOptions_key; @@ -126,25 +126,30 @@ extern NSString *const TxClientOptions_fallbackRetryTimeout; extern NSString *const TxClientOptions_defaultTokenParams; extern NSString *const TxClientOptions_channelRetryTimeout; extern NSString *const TxClientOptions_transportParams; -@end -@interface TxTokenDetails : NSObject +// key constants for RestChannelOptions +extern NSString *const TxRestChannelOptions_cipher; + +// key constants for RealtimeChannelOptions +extern NSString *const TxRealtimeChannelOptions_cipher; +extern NSString *const TxRealtimeChannelOptions_params; +extern NSString *const TxRealtimeChannelOptions_modes; + +// key constants for TokenDetails extern NSString *const TxTokenDetails_token; extern NSString *const TxTokenDetails_expires; extern NSString *const TxTokenDetails_issued; extern NSString *const TxTokenDetails_capability; extern NSString *const TxTokenDetails_clientId; -@end -@interface TxTokenParams : NSObject +// key constants for TokenParams extern NSString *const TxTokenParams_capability; extern NSString *const TxTokenParams_clientId; extern NSString *const TxTokenParams_nonce; extern NSString *const TxTokenParams_timestamp; extern NSString *const TxTokenParams_ttl; -@end -@interface TxTokenRequest : NSObject +// key constants for TokenRequest extern NSString *const TxTokenRequest_capability; extern NSString *const TxTokenRequest_clientId; extern NSString *const TxTokenRequest_keyName; @@ -152,9 +157,8 @@ extern NSString *const TxTokenRequest_mac; extern NSString *const TxTokenRequest_nonce; extern NSString *const TxTokenRequest_timestamp; extern NSString *const TxTokenRequest_ttl; -@end -@interface TxEnumConstants : NSObject +// key constants for EnumConstants extern NSString *const TxEnumConstants_initialized; extern NSString *const TxEnumConstants_connecting; extern NSString *const TxEnumConstants_connected; @@ -172,25 +176,26 @@ extern NSString *const TxEnumConstants_leave; extern NSString *const TxEnumConstants_enter; extern NSString *const TxEnumConstants_present; extern NSString *const TxEnumConstants_update; -@end +extern NSString *const TxEnumConstants_presence; +extern NSString *const TxEnumConstants_publish; +extern NSString *const TxEnumConstants_subscribe; +extern NSString *const TxEnumConstants_presenceSubscribe; -@interface TxConnectionStateChange : NSObject +// key constants for ConnectionStateChange extern NSString *const TxConnectionStateChange_current; extern NSString *const TxConnectionStateChange_previous; extern NSString *const TxConnectionStateChange_event; extern NSString *const TxConnectionStateChange_retryIn; extern NSString *const TxConnectionStateChange_reason; -@end -@interface TxChannelStateChange : NSObject +// key constants for ChannelStateChange extern NSString *const TxChannelStateChange_current; extern NSString *const TxChannelStateChange_previous; extern NSString *const TxChannelStateChange_event; extern NSString *const TxChannelStateChange_resumed; extern NSString *const TxChannelStateChange_reason; -@end -@interface TxMessage : NSObject +// key constants for Message extern NSString *const TxMessage_id; extern NSString *const TxMessage_timestamp; extern NSString *const TxMessage_clientId; @@ -199,9 +204,8 @@ extern NSString *const TxMessage_encoding; extern NSString *const TxMessage_data; extern NSString *const TxMessage_name; extern NSString *const TxMessage_extras; -@end -@interface TxPresenceMessage : NSObject +// key constants for PresenceMessage extern NSString *const TxPresenceMessage_id; extern NSString *const TxPresenceMessage_action; extern NSString *const TxPresenceMessage_clientId; @@ -210,37 +214,31 @@ extern NSString *const TxPresenceMessage_data; extern NSString *const TxPresenceMessage_encoding; extern NSString *const TxPresenceMessage_extras; extern NSString *const TxPresenceMessage_timestamp; -@end -@interface TxPaginatedResult : NSObject +// key constants for PaginatedResult extern NSString *const TxPaginatedResult_items; extern NSString *const TxPaginatedResult_type; extern NSString *const TxPaginatedResult_hasNext; -@end -@interface TxRestHistoryParams : NSObject +// key constants for RestHistoryParams extern NSString *const TxRestHistoryParams_start; extern NSString *const TxRestHistoryParams_end; extern NSString *const TxRestHistoryParams_direction; extern NSString *const TxRestHistoryParams_limit; -@end -@interface TxRealtimeHistoryParams : NSObject +// key constants for RealtimeHistoryParams extern NSString *const TxRealtimeHistoryParams_start; extern NSString *const TxRealtimeHistoryParams_end; extern NSString *const TxRealtimeHistoryParams_direction; extern NSString *const TxRealtimeHistoryParams_limit; extern NSString *const TxRealtimeHistoryParams_untilAttach; -@end -@interface TxRestPresenceParams : NSObject +// key constants for RestPresenceParams extern NSString *const TxRestPresenceParams_limit; extern NSString *const TxRestPresenceParams_clientId; extern NSString *const TxRestPresenceParams_connectionId; -@end -@interface TxRealtimePresenceParams : NSObject +// key constants for RealtimePresenceParams extern NSString *const TxRealtimePresenceParams_waitForSync; extern NSString *const TxRealtimePresenceParams_clientId; extern NSString *const TxRealtimePresenceParams_connectionId; -@end diff --git a/ios/Classes/codec/AblyPlatformConstants.m b/ios/Classes/codec/AblyPlatformConstants.m index 72a92b1bf..443aa4757 100644 --- a/ios/Classes/codec/AblyPlatformConstants.m +++ b/ios/Classes/codec/AblyPlatformConstants.m @@ -6,17 +6,19 @@ #import "AblyPlatformConstants.h" -@implementation AblyPlatformMethod +// flutter platform channel method names NSString *const AblyPlatformMethod_getPlatformVersion= @"getPlatformVersion"; NSString *const AblyPlatformMethod_getVersion= @"getVersion"; NSString *const AblyPlatformMethod_registerAbly= @"registerAbly"; NSString *const AblyPlatformMethod_authCallback= @"authCallback"; NSString *const AblyPlatformMethod_realtimeAuthCallback= @"realtimeAuthCallback"; NSString *const AblyPlatformMethod_createRestWithOptions= @"createRestWithOptions"; +NSString *const AblyPlatformMethod_setRestChannelOptions= @"setRestChannelOptions"; NSString *const AblyPlatformMethod_publish= @"publish"; NSString *const AblyPlatformMethod_restHistory= @"restHistory"; NSString *const AblyPlatformMethod_restPresenceGet= @"restPresenceGet"; NSString *const AblyPlatformMethod_restPresenceHistory= @"restPresenceHistory"; +NSString *const AblyPlatformMethod_releaseRestChannel= @"releaseRestChannel"; NSString *const AblyPlatformMethod_createRealtimeWithOptions= @"createRealtimeWithOptions"; NSString *const AblyPlatformMethod_connectRealtime= @"connectRealtime"; NSString *const AblyPlatformMethod_closeRealtime= @"closeRealtime"; @@ -30,49 +32,45 @@ @implementation AblyPlatformMethod NSString *const AblyPlatformMethod_realtimePresenceLeave= @"realtimePresenceLeave"; NSString *const AblyPlatformMethod_onRealtimePresenceMessage= @"onRealtimePresenceMessage"; NSString *const AblyPlatformMethod_publishRealtimeChannelMessage= @"publishRealtimeChannelMessage"; +NSString *const AblyPlatformMethod_releaseRealtimeChannel= @"releaseRealtimeChannel"; NSString *const AblyPlatformMethod_realtimeHistory= @"realtimeHistory"; NSString *const AblyPlatformMethod_onRealtimeConnectionStateChanged= @"onRealtimeConnectionStateChanged"; NSString *const AblyPlatformMethod_onRealtimeChannelStateChanged= @"onRealtimeChannelStateChanged"; NSString *const AblyPlatformMethod_onRealtimeChannelMessage= @"onRealtimeChannelMessage"; NSString *const AblyPlatformMethod_nextPage= @"nextPage"; NSString *const AblyPlatformMethod_firstPage= @"firstPage"; -@end -@implementation TxTransportKeys +// key constants for TransportKeys NSString *const TxTransportKeys_channelName = @"channelName"; NSString *const TxTransportKeys_params = @"params"; NSString *const TxTransportKeys_data = @"data"; NSString *const TxTransportKeys_clientId = @"clientId"; NSString *const TxTransportKeys_options = @"options"; -@end +NSString *const TxTransportKeys_messages = @"messages"; -@implementation TxAblyMessage +// key constants for AblyMessage NSString *const TxAblyMessage_registrationHandle = @"registrationHandle"; NSString *const TxAblyMessage_type = @"type"; NSString *const TxAblyMessage_message = @"message"; -@end -@implementation TxAblyEventMessage +// key constants for AblyEventMessage NSString *const TxAblyEventMessage_eventName = @"eventName"; NSString *const TxAblyEventMessage_type = @"type"; NSString *const TxAblyEventMessage_message = @"message"; -@end -@implementation TxErrorInfo +// key constants for ErrorInfo NSString *const TxErrorInfo_code = @"code"; NSString *const TxErrorInfo_message = @"message"; NSString *const TxErrorInfo_statusCode = @"statusCode"; NSString *const TxErrorInfo_href = @"href"; NSString *const TxErrorInfo_requestId = @"requestId"; NSString *const TxErrorInfo_cause = @"cause"; -@end -@implementation TxMessageData +// key constants for MessageData NSString *const TxMessageData_data = @"data"; NSString *const TxMessageData_type = @"type"; -@end -@implementation TxClientOptions +// key constants for ClientOptions NSString *const TxClientOptions_authUrl = @"authUrl"; NSString *const TxClientOptions_authMethod = @"authMethod"; NSString *const TxClientOptions_key = @"key"; @@ -106,25 +104,30 @@ @implementation TxClientOptions NSString *const TxClientOptions_defaultTokenParams = @"defaultTokenParams"; NSString *const TxClientOptions_channelRetryTimeout = @"channelRetryTimeout"; NSString *const TxClientOptions_transportParams = @"transportParams"; -@end -@implementation TxTokenDetails +// key constants for RestChannelOptions +NSString *const TxRestChannelOptions_cipher = @"cipher"; + +// key constants for RealtimeChannelOptions +NSString *const TxRealtimeChannelOptions_cipher = @"cipher"; +NSString *const TxRealtimeChannelOptions_params = @"params"; +NSString *const TxRealtimeChannelOptions_modes = @"modes"; + +// key constants for TokenDetails NSString *const TxTokenDetails_token = @"token"; NSString *const TxTokenDetails_expires = @"expires"; NSString *const TxTokenDetails_issued = @"issued"; NSString *const TxTokenDetails_capability = @"capability"; NSString *const TxTokenDetails_clientId = @"clientId"; -@end -@implementation TxTokenParams +// key constants for TokenParams NSString *const TxTokenParams_capability = @"capability"; NSString *const TxTokenParams_clientId = @"clientId"; NSString *const TxTokenParams_nonce = @"nonce"; NSString *const TxTokenParams_timestamp = @"timestamp"; NSString *const TxTokenParams_ttl = @"ttl"; -@end -@implementation TxTokenRequest +// key constants for TokenRequest NSString *const TxTokenRequest_capability = @"capability"; NSString *const TxTokenRequest_clientId = @"clientId"; NSString *const TxTokenRequest_keyName = @"keyName"; @@ -132,9 +135,8 @@ @implementation TxTokenRequest NSString *const TxTokenRequest_nonce = @"nonce"; NSString *const TxTokenRequest_timestamp = @"timestamp"; NSString *const TxTokenRequest_ttl = @"ttl"; -@end -@implementation TxEnumConstants +// key constants for EnumConstants NSString *const TxEnumConstants_initialized = @"initialized"; NSString *const TxEnumConstants_connecting = @"connecting"; NSString *const TxEnumConstants_connected = @"connected"; @@ -152,25 +154,26 @@ @implementation TxEnumConstants NSString *const TxEnumConstants_enter = @"enter"; NSString *const TxEnumConstants_present = @"present"; NSString *const TxEnumConstants_update = @"update"; -@end +NSString *const TxEnumConstants_presence = @"presence"; +NSString *const TxEnumConstants_publish = @"publish"; +NSString *const TxEnumConstants_subscribe = @"subscribe"; +NSString *const TxEnumConstants_presenceSubscribe = @"presenceSubscribe"; -@implementation TxConnectionStateChange +// key constants for ConnectionStateChange NSString *const TxConnectionStateChange_current = @"current"; NSString *const TxConnectionStateChange_previous = @"previous"; NSString *const TxConnectionStateChange_event = @"event"; NSString *const TxConnectionStateChange_retryIn = @"retryIn"; NSString *const TxConnectionStateChange_reason = @"reason"; -@end -@implementation TxChannelStateChange +// key constants for ChannelStateChange NSString *const TxChannelStateChange_current = @"current"; NSString *const TxChannelStateChange_previous = @"previous"; NSString *const TxChannelStateChange_event = @"event"; NSString *const TxChannelStateChange_resumed = @"resumed"; NSString *const TxChannelStateChange_reason = @"reason"; -@end -@implementation TxMessage +// key constants for Message NSString *const TxMessage_id = @"id"; NSString *const TxMessage_timestamp = @"timestamp"; NSString *const TxMessage_clientId = @"clientId"; @@ -179,9 +182,8 @@ @implementation TxMessage NSString *const TxMessage_data = @"data"; NSString *const TxMessage_name = @"name"; NSString *const TxMessage_extras = @"extras"; -@end -@implementation TxPresenceMessage +// key constants for PresenceMessage NSString *const TxPresenceMessage_id = @"id"; NSString *const TxPresenceMessage_action = @"action"; NSString *const TxPresenceMessage_clientId = @"clientId"; @@ -190,37 +192,31 @@ @implementation TxPresenceMessage NSString *const TxPresenceMessage_encoding = @"encoding"; NSString *const TxPresenceMessage_extras = @"extras"; NSString *const TxPresenceMessage_timestamp = @"timestamp"; -@end -@implementation TxPaginatedResult +// key constants for PaginatedResult NSString *const TxPaginatedResult_items = @"items"; NSString *const TxPaginatedResult_type = @"type"; NSString *const TxPaginatedResult_hasNext = @"hasNext"; -@end -@implementation TxRestHistoryParams +// key constants for RestHistoryParams NSString *const TxRestHistoryParams_start = @"start"; NSString *const TxRestHistoryParams_end = @"end"; NSString *const TxRestHistoryParams_direction = @"direction"; NSString *const TxRestHistoryParams_limit = @"limit"; -@end -@implementation TxRealtimeHistoryParams +// key constants for RealtimeHistoryParams NSString *const TxRealtimeHistoryParams_start = @"start"; NSString *const TxRealtimeHistoryParams_end = @"end"; NSString *const TxRealtimeHistoryParams_direction = @"direction"; NSString *const TxRealtimeHistoryParams_limit = @"limit"; NSString *const TxRealtimeHistoryParams_untilAttach = @"untilAttach"; -@end -@implementation TxRestPresenceParams +// key constants for RestPresenceParams NSString *const TxRestPresenceParams_limit = @"limit"; NSString *const TxRestPresenceParams_clientId = @"clientId"; NSString *const TxRestPresenceParams_connectionId = @"connectionId"; -@end -@implementation TxRealtimePresenceParams +// key constants for RealtimePresenceParams NSString *const TxRealtimePresenceParams_waitForSync = @"waitForSync"; NSString *const TxRealtimePresenceParams_clientId = @"clientId"; NSString *const TxRealtimePresenceParams_connectionId = @"connectionId"; -@end diff --git a/ios/ably_flutter.podspec b/ios/ably_flutter.podspec index 83c612cc4..5ca176d2a 100644 --- a/ios/ably_flutter.podspec +++ b/ios/ably_flutter.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'Ably' + s.dependency 'Ably', '1.2.4' s.platform = :ios s.ios.deployment_target = '8.0' diff --git a/lib/src/codec.dart b/lib/src/codec.dart index 0377da372..2c19e97db 100644 --- a/lib/src/codec.dart +++ b/lib/src/codec.dart @@ -4,6 +4,8 @@ import 'package:flutter/services.dart'; import '../ably_flutter.dart'; import '../src/generated/platformconstants.dart'; import '../src/impl/message.dart'; +import '../src/spec/realtime/channels.dart'; +import '../src/spec/rest/channels.dart'; /// a [_Encoder] encodes custom type and converts it to a Map which will /// be passed on to platform side @@ -65,6 +67,12 @@ class Codec extends StandardMessageCodec { _CodecPair(_encodeTokenDetails, _decodeTokenDetails), CodecTypes.tokenRequest: _CodecPair(_encodeTokenRequest, null), + CodecTypes.restChannelOptions: + _CodecPair(_encodeRestChannelOptions, null), + CodecTypes.realtimeChannelOptions: _CodecPair( + _encodeRealtimeChannelOptions, + null, + ), CodecTypes.paginatedResult: _CodecPair(null, _decodePaginatedResult), CodecTypes.realtimeHistoryParams: @@ -78,7 +86,8 @@ class Codec extends StandardMessageCodec { null, ), - CodecTypes.errorInfo: _CodecPair(null, _decodeErrorInfo), + CodecTypes.errorInfo: + _CodecPair(_encodeErrorInfo, _decodeErrorInfo), CodecTypes.messageData: _CodecPair( _encodeChannelMessageData, _decodeChannelMessageData, @@ -176,7 +185,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [ClientOptions] to a Map - /// returns null of passed value [v] is null + /// returns null of [v] is null Map _encodeClientOptions(final ClientOptions v) { if (v == null) return null; final jsonMap = {}; @@ -232,7 +241,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [TokenDetails] to a Map - /// returns null of passed value [v] is null + /// returns null if [v] is null Map _encodeTokenDetails(final TokenDetails v) { if (v == null) return null; return { @@ -245,7 +254,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [TokenParams] to a Map - /// returns null of passed value [v] is null + /// returns null if [v] is null Map _encodeTokenParams(final TokenParams v) { if (v == null) return null; final jsonMap = {}; @@ -258,7 +267,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [TokenRequest] to a Map - /// returns null of passed value [v] is null + /// returns null if [v] is null Map _encodeTokenRequest(final TokenRequest v) { if (v == null) return null; final jsonMap = {}; @@ -273,8 +282,48 @@ class Codec extends StandardMessageCodec { return jsonMap; } + /// Encodes [ChannelOptions] to a Map + /// returns null if [v] is null + Map _encodeRestChannelOptions(final ChannelOptions v) { + if (v == null) return null; + final jsonMap = {}; + _writeToJson(jsonMap, TxRestChannelOptions.cipher, v.cipher); + return jsonMap; + } + + /// Encodes [ChannelMode] to a string constant + String _encodeChannelMode(ChannelMode mode) { + switch (mode) { + case ChannelMode.presence: + return TxEnumConstants.presence; + case ChannelMode.publish: + return TxEnumConstants.publish; + case ChannelMode.subscribe: + return TxEnumConstants.subscribe; + case ChannelMode.presenceSubscribe: + return TxEnumConstants.presenceSubscribe; + } + return null; + } + + /// Encodes [RealtimeChannelOptions] to a Map + /// returns null if [v] is null + Map _encodeRealtimeChannelOptions( + final RealtimeChannelOptions v) { + if (v == null) return null; + final jsonMap = {}; + _writeToJson(jsonMap, TxRealtimeChannelOptions.cipher, v.cipher); + _writeToJson(jsonMap, TxRealtimeChannelOptions.params, v.params); + _writeToJson( + jsonMap, + TxRealtimeChannelOptions.modes, + v.modes?.map(_encodeChannelMode)?.toList(), + ); + return jsonMap; + } + /// Encodes [RestHistoryParams] to a Map - /// returns null of passed value [v] is null + /// returns null if [v] is null Map _encodeRestHistoryParams(final RestHistoryParams v) { if (v == null) return null; final jsonMap = {}; @@ -287,6 +336,8 @@ class Codec extends StandardMessageCodec { return jsonMap; } + /// Encodes [RestPresenceParams] to a Map + /// returns null if [v] is null Map _encodeRestPresenceParams(final RestPresenceParams v) { if (v == null) return null; final jsonMap = {}; @@ -308,7 +359,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [RealtimeHistoryParams] to a Map - /// returns null of passed value [v] is null + /// returns null of [v] is null Map _encodeRealtimeHistoryParams( final RealtimeHistoryParams v) { if (v == null) return null; @@ -324,7 +375,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [AblyMessage] to a Map - /// returns null of passed value [v] is null + /// returns null of [v] is null Map _encodeAblyMessage(final AblyMessage v) { if (v == null) return null; final codecType = getCodecType(v.message); @@ -341,7 +392,7 @@ class Codec extends StandardMessageCodec { } /// Encodes [AblyEventMessage] to a Map - /// returns null of passed value [v] is null + /// returns null of [v] is null Map _encodeAblyEventMessage(final AblyEventMessage v) { if (v == null) return null; final codecType = getCodecType(v.message); @@ -357,6 +408,22 @@ class Codec extends StandardMessageCodec { return jsonMap; } + /// Encodes [ErrorInfo] to a Map + /// returns null of [v] is null + Map _encodeErrorInfo(final ErrorInfo v) { + if (v == null) return null; + final jsonMap = {}; + _writeToJson(jsonMap, TxErrorInfo.code, v.code); + _writeToJson(jsonMap, TxErrorInfo.message, v.message); + _writeToJson(jsonMap, TxErrorInfo.statusCode, v.statusCode); + _writeToJson(jsonMap, TxErrorInfo.href, v.href); + _writeToJson(jsonMap, TxErrorInfo.requestId, v.requestId); + _writeToJson(jsonMap, TxErrorInfo.cause, v.cause); + return jsonMap; + } + + /// Encodes [MessageData] to a Map + /// returns null of [v] is null Map _encodeChannelMessageData(final MessageData v) { if (v == null) return null; final jsonMap = {}; @@ -364,6 +431,8 @@ class Codec extends StandardMessageCodec { return jsonMap; } + /// Encodes [Message] to a Map + /// returns null of [v] is null Map _encodeChannelMessage(final Message v) { if (v == null) return null; final jsonMap = {}; @@ -715,12 +784,16 @@ class Codec extends StandardMessageCodec { resumed: resumed, reason: reason); } + /// Decodes value [jsonMap] to [MessageData] + /// returns null if [jsonMap] is null MessageData _decodeChannelMessageData(Map jsonMap) { if (jsonMap == null) return null; return MessageData.fromValue( _readFromJson(jsonMap, TxMessageData.data)); } + /// Decodes value [jsonMap] to [Message] + /// returns null if [jsonMap] is null Message _decodeChannelMessage(Map jsonMap) { if (jsonMap == null) return null; final timestamp = _readFromJson(jsonMap, TxMessage.timestamp); @@ -756,6 +829,8 @@ class Codec extends StandardMessageCodec { } } + /// Decodes value [jsonMap] to [PresenceMessage] + /// returns null if [jsonMap] is null PresenceMessage _decodePresenceMessage(Map jsonMap) { if (jsonMap == null) return null; final timestamp = _readFromJson(jsonMap, TxPresenceMessage.timestamp); @@ -777,6 +852,8 @@ class Codec extends StandardMessageCodec { ); } + /// Decodes value [jsonMap] to [PaginatedResult] + /// returns null if [jsonMap] is null PaginatedResult _decodePaginatedResult(Map jsonMap) { if (jsonMap == null) return null; final type = _readFromJson(jsonMap, TxPaginatedResult.type); diff --git a/lib/src/generated/platformconstants.dart b/lib/src/generated/platformconstants.dart index 1c360f8fd..876ec2798 100644 --- a/lib/src/generated/platformconstants.dart +++ b/lib/src/generated/platformconstants.dart @@ -14,15 +14,17 @@ class CodecTypes { static const int tokenParams = 133; static const int tokenDetails = 134; static const int tokenRequest = 135; - static const int paginatedResult = 136; - static const int restHistoryParams = 137; - static const int realtimeHistoryParams = 138; - static const int restPresenceParams = 139; - static const int presenceMessage = 140; - static const int realtimePresenceParams = 141; - static const int errorInfo = 142; - static const int connectionStateChange = 143; - static const int channelStateChange = 144; + static const int restChannelOptions = 136; + static const int realtimeChannelOptions = 137; + static const int paginatedResult = 138; + static const int restHistoryParams = 139; + static const int realtimeHistoryParams = 140; + static const int restPresenceParams = 141; + static const int presenceMessage = 142; + static const int realtimePresenceParams = 143; + static const int errorInfo = 144; + static const int connectionStateChange = 145; + static const int channelStateChange = 146; } class PlatformMethod { @@ -32,10 +34,12 @@ class PlatformMethod { static const String authCallback = 'authCallback'; static const String realtimeAuthCallback = 'realtimeAuthCallback'; static const String createRestWithOptions = 'createRestWithOptions'; + static const String setRestChannelOptions = 'setRestChannelOptions'; static const String publish = 'publish'; static const String restHistory = 'restHistory'; static const String restPresenceGet = 'restPresenceGet'; static const String restPresenceHistory = 'restPresenceHistory'; + static const String releaseRestChannel = 'releaseRestChannel'; static const String createRealtimeWithOptions = 'createRealtimeWithOptions'; static const String connectRealtime = 'connectRealtime'; static const String closeRealtime = 'closeRealtime'; @@ -50,6 +54,7 @@ class PlatformMethod { static const String onRealtimePresenceMessage = 'onRealtimePresenceMessage'; static const String publishRealtimeChannelMessage = 'publishRealtimeChannelMessage'; + static const String releaseRealtimeChannel = 'releaseRealtimeChannel'; static const String realtimeHistory = 'realtimeHistory'; static const String onRealtimeConnectionStateChanged = 'onRealtimeConnectionStateChanged'; @@ -66,6 +71,7 @@ class TxTransportKeys { static const String data = 'data'; static const String clientId = 'clientId'; static const String options = 'options'; + static const String messages = 'messages'; } class TxAblyMessage { @@ -130,6 +136,16 @@ class TxClientOptions { static const String transportParams = 'transportParams'; } +class TxRestChannelOptions { + static const String cipher = 'cipher'; +} + +class TxRealtimeChannelOptions { + static const String cipher = 'cipher'; + static const String params = 'params'; + static const String modes = 'modes'; +} + class TxTokenDetails { static const String token = 'token'; static const String expires = 'expires'; @@ -174,6 +190,10 @@ class TxEnumConstants { static const String enter = 'enter'; static const String present = 'present'; static const String update = 'update'; + static const String presence = 'presence'; + static const String publish = 'publish'; + static const String subscribe = 'subscribe'; + static const String presenceSubscribe = 'presenceSubscribe'; } class TxConnectionStateChange { diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index c4ce397ce..30158edcc 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import '../platform.dart' as platform; +import '../spec/constants.dart'; import 'message.dart'; import 'streams_channel.dart'; @@ -11,7 +12,6 @@ import 'streams_channel.dart'; /// where that live counterpart is held as a strong reference by the plugin /// implementation. abstract class PlatformObject { - static const _acquireHandleTimeout = Duration(seconds: 5); Future _handle; int _handleValue; // Only for logging. Otherwise use _handle instead. @@ -37,14 +37,16 @@ abstract class PlatformObject { /// updates [_handle] and returns it. Future get handle async => _handle ??= _acquireHandle(); - Future _acquireHandle() => - createPlatformInstance().timeout(_acquireHandleTimeout, onTimeout: () { - _handle = null; - throw TimeoutException( - 'Acquiring handle timed out.', - _acquireHandleTimeout, - ); - }).then((value) => _handleValue = value); + Future _acquireHandle() => createPlatformInstance().timeout( + Timeouts.acquireHandleTimeout, + onTimeout: () { + _handle = null; + throw TimeoutException( + 'Acquiring handle timed out.', + Timeouts.acquireHandleTimeout, + ); + }, + ).then((value) => _handleValue = value); /// [MethodChannel] to make method calls to platform side MethodChannel get methodChannel => platform.methodChannel; @@ -58,7 +60,6 @@ abstract class PlatformObject { platform.invokePlatformMethod(method, arguments); /// invoke platform method channel with AblyMessage encapsulation - @protected Future invoke(final String method, [final Object argument]) async { final _handle = await handle; final message = (null != argument) diff --git a/lib/src/impl/realtime/channels.dart b/lib/src/impl/realtime/channels.dart index 133564422..3dee6e007 100644 --- a/lib/src/impl/realtime/channels.dart +++ b/lib/src/impl/realtime/channels.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; import 'package:pedantic/pedantic.dart'; import '../../../ably_flutter.dart'; @@ -15,24 +16,21 @@ import 'realtime.dart'; class RealtimeChannel extends PlatformObject implements RealtimeChannelInterface { @override - RealtimeInterface realtime; + final RealtimeInterface realtime; @override - String name; - - @override - ChannelOptions options; + final String name; RealtimePresence _presence; @override RealtimePresence get presence => _presence; - /// instantiates with [Rest], [name] and [ChannelOptions] + /// instantiates with [Rest], [name] and [RealtimeChannelOptions] /// /// sets default [state] to [ChannelState.initialized] and start listening /// for updates to the channel [state]/ - RealtimeChannel(this.realtime, this.name, this.options) : super() { + RealtimeChannel(this.realtime, this.name) : super() { _presence = RealtimePresence(this); state = ChannelState.initialized; on().listen((event) => state = event.current); @@ -55,13 +53,6 @@ class RealtimeChannel extends PlatformObject return PaginatedResult.fromAblyMessage(message); } - Map __payload; - - Map get _payload => __payload ??= { - 'channel': name, - if (options != null) 'options': options, - }; - final _publishQueue = Queue<_PublishQueueItem>(); Completer _authCallbackCompleter; @@ -102,8 +93,8 @@ class RealtimeChannel extends PlatformObject try { await invoke(PlatformMethod.publishRealtimeChannelMessage, { - ..._payload, - 'messages': item.messages, + TxTransportKeys.channelName: name, + TxTransportKeys.messages: item.messages, }); _publishQueue.remove(item); @@ -113,8 +104,8 @@ class RealtimeChannel extends PlatformObject if (!item.completer.isCompleted) { item.completer.complete(); } - } on PlatformException catch (pe) { - if (pe.code == ErrorCodes.authCallbackFailure.toString()) { + } on AblyException catch (ae) { + if (ae.code == ErrorCodes.authCallbackFailure.toString()) { if (_authCallbackCompleter != null) { return; } @@ -136,12 +127,11 @@ class RealtimeChannel extends PlatformObject } else { _publishQueue .where((e) => !e.completer.isCompleted) - .forEach((e) => e.completer.completeError(AblyException( - pe.code, - pe.message, - pe.details as ErrorInfo, - ))); + .forEach((e) => e.completer.completeError(ae)); } + } on PlatformException catch (pe) { + _publishQueue.where((e) => !e.completer.isCompleted).forEach((e) => + e.completer.completeError(AblyException.fromPlatformException(pe))); } on Exception { // removing item from queue and rethrowing exception _publishQueue.remove(item); @@ -177,33 +167,29 @@ class RealtimeChannel extends PlatformObject ChannelState state; @override - Future attach() async { - try { - await invoke(PlatformMethod.attachRealtimeChannel, _payload); - } on PlatformException catch (pe) { - throw AblyException(pe.code, pe.message, pe.details as ErrorInfo); - } - } + Future attach() => invoke(PlatformMethod.attachRealtimeChannel, { + TxTransportKeys.channelName: name, + }); @override - Future detach() async { - try { - await invoke(PlatformMethod.detachRealtimeChannel, _payload); - } on PlatformException catch (pe) { - throw AblyException(pe.code, pe.message, pe.details as ErrorInfo); - } - } + Future detach() => invoke(PlatformMethod.detachRealtimeChannel, { + TxTransportKeys.channelName: name, + }); @override - Future setOptions(ChannelOptions options) async { - throw UnimplementedError(); - } + Future setOptions(RealtimeChannelOptions options) => + invoke(PlatformMethod.setRealtimeChannelOptions, { + TxTransportKeys.channelName: name, + TxTransportKeys.options: options, + }); @override Stream on([ChannelEvent channelEvent]) => listen( PlatformMethod.onRealtimeChannelStateChanged, - _payload, + { + TxTransportKeys.channelName: name, + }, ).where( (stateChange) => channelEvent == null || stateChange.event == channelEvent, @@ -212,10 +198,11 @@ class RealtimeChannel extends PlatformObject @override Stream subscribe({String name, List names}) { final subscribedNames = {name, ...?names}.where((n) => n != null).toList(); - return listen(PlatformMethod.onRealtimeChannelMessage, _payload) - .where((message) => - subscribedNames.isEmpty || - subscribedNames.any((n) => n == message.name)); + return listen(PlatformMethod.onRealtimeChannelMessage, { + TxTransportKeys.channelName: this.name, + }).where((message) => + subscribedNames.isEmpty || + subscribedNames.any((n) => n == message.name)); } } @@ -227,8 +214,14 @@ class RealtimePlatformChannels extends RealtimeChannels { RealtimePlatformChannels(Realtime realtime) : super(realtime); @override - RealtimeChannel createChannel(String name, ChannelOptions options) => - RealtimeChannel(realtime, name, options); + @protected + RealtimeChannel createChannel(String name) => RealtimeChannel(realtime, name); + + @override + void release(String name) { + super.release(name); + (realtime as Realtime).invoke(PlatformMethod.releaseRealtimeChannel, name); + } } /// An item for used to enqueue a message to be published after an ongoing diff --git a/lib/src/impl/realtime/connection.dart b/lib/src/impl/realtime/connection.dart index d8a51c0af..95a2e12fb 100644 --- a/lib/src/impl/realtime/connection.dart +++ b/lib/src/impl/realtime/connection.dart @@ -9,19 +9,21 @@ import 'realtime.dart'; /// connects to Ably service class Connection extends PlatformObject implements ConnectionInterface { /// Realtime client instance - Realtime realtime; + final Realtime realtime; + + ConnectionState _state; /// instantiates a connection with [realtime] client instance /// /// sets default [state] to [ConnectionState.initialized] and starts listening /// for updates to the connection [state]. Connection(this.realtime) : super() { - state = ConnectionState.initialized; + _state = ConnectionState.initialized; on().listen((event) { if (event.reason?.code == ErrorCodes.authCallbackFailure) { realtime.awaitAuthUpdateAndReconnect(); } - state = event.current; + _state = event.current; }); } @@ -44,7 +46,7 @@ class Connection extends PlatformObject implements ConnectionInterface { int serial; @override - ConnectionState state; + ConnectionState get state => _state; @override Stream on([ConnectionEvent connectionEvent]) => diff --git a/lib/src/impl/realtime/realtime.dart b/lib/src/impl/realtime/realtime.dart index 26bc7490f..1ed1c5c8b 100644 --- a/lib/src/impl/realtime/realtime.dart +++ b/lib/src/impl/realtime/realtime.dart @@ -151,7 +151,7 @@ class Realtime extends PlatformObject /// discussion: https://github.com/ably/ably-flutter/issues/31 void authUpdateComplete() { _authCallbackCompleter?.complete(); - for (final channel in channels.all) { + for (final channel in channels) { channel.authUpdateComplete(); } } diff --git a/lib/src/impl/rest/channels.dart b/lib/src/impl/rest/channels.dart index 2767c7ae7..50379d49f 100644 --- a/lib/src/impl/rest/channels.dart +++ b/lib/src/impl/rest/channels.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; import 'package:pedantic/pedantic.dart'; import '../../../ably_flutter.dart'; @@ -18,13 +19,10 @@ class RestChannel extends PlatformObject implements RestChannelInterface { @override String name; - @override - ChannelOptions options; - RestPresence _presence; /// instantiates with [Rest], [name] and [ChannelOptions] - RestChannel(this.rest, this.name, this.options) { + RestChannel(this.rest, this.name) { _presence = RestPresence(this); } @@ -88,8 +86,8 @@ class RestChannel extends PlatformObject implements RestChannelInterface { try { final _map = { - 'channel': name, - 'messages': item.messages + TxTransportKeys.channelName: name, + TxTransportKeys.messages: item.messages, }; await invoke(PlatformMethod.publish, _map); @@ -101,8 +99,8 @@ class RestChannel extends PlatformObject implements RestChannelInterface { if (!item.completer.isCompleted) { item.completer.complete(); } - } on PlatformException catch (pe) { - if (pe.code == ErrorCodes.authCallbackFailure.toString()) { + } on AblyException catch (ae) { + if (ae.code == ErrorCodes.authCallbackFailure.toString()) { if (_authCallbackCompleter != null) { return; } @@ -125,12 +123,11 @@ class RestChannel extends PlatformObject implements RestChannelInterface { } else { _publishQueue .where((e) => !e.completer.isCompleted) - .forEach((e) => e.completer.completeError(AblyException( - pe.code, - pe.message, - pe.details as ErrorInfo, - ))); + .forEach((e) => e.completer.completeError(ae)); } + } on PlatformException catch (pe) { + _publishQueue.where((e) => !e.completer.isCompleted).forEach((e) => + e.completer.completeError(AblyException.fromPlatformException(pe))); } on Exception { // removing item from queue and rethrowing exception _publishQueue.remove(item); @@ -151,9 +148,11 @@ class RestChannel extends PlatformObject implements RestChannelInterface { } @override - Future setOptions(ChannelOptions options) async { - throw UnimplementedError(); - } + Future setOptions(ChannelOptions options) => + invoke(PlatformMethod.setRealtimeChannelOptions, { + TxTransportKeys.channelName: name, + TxTransportKeys.options: options, + }); } /// A collection of rest channel objects @@ -164,8 +163,14 @@ class RestPlatformChannels extends RestChannels { RestPlatformChannels(Rest rest) : super(rest); @override - RestChannel createChannel(String name, ChannelOptions options) => - RestChannel(rest, name, options); + @protected + RestChannel createChannel(String name) => RestChannel(rest, name); + + @override + void release(String name) { + super.release(name); + (rest as Rest).invoke(PlatformMethod.releaseRestChannel, name); + } } /// An item for used to enqueue a message to be published after an ongoing diff --git a/lib/src/impl/rest/rest.dart b/lib/src/impl/rest/rest.dart index 778e4968b..c20e20d72 100644 --- a/lib/src/impl/rest/rest.dart +++ b/lib/src/impl/rest/rest.dart @@ -48,7 +48,7 @@ class Rest extends PlatformObject /// /// discussion: https://github.com/ably/ably-flutter/issues/31 void authUpdateComplete() { - for (final channel in channels.all) { + for (final channel in channels) { channel.authUpdateComplete(); } } diff --git a/lib/src/impl/streams_channel.dart b/lib/src/impl/streams_channel.dart index 7c3178b24..f0f8d3a14 100644 --- a/lib/src/impl/streams_channel.dart +++ b/lib/src/impl/streams_channel.dart @@ -23,6 +23,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '../spec/common.dart'; + /// Manages multiple event listeners which would otherwise require verbose code /// on platform side class StreamsChannel { @@ -54,8 +56,12 @@ class StreamsChannel { } else { try { controller.add(codec.decodeEnvelope(reply) as T); - } on PlatformException catch (e) { - controller.addError(e); + } on PlatformException catch (pe) { + if (pe.details is ErrorInfo) { + throw AblyException.fromPlatformException(pe); + } else { + controller.addError(pe); + } } } diff --git a/lib/src/platform.dart b/lib/src/platform.dart index 759c38811..b09367d1d 100644 --- a/lib/src/platform.dart +++ b/lib/src/platform.dart @@ -6,6 +6,8 @@ import 'codec.dart'; import 'generated/platformconstants.dart' show PlatformMethod; import 'impl/streams_channel.dart'; import 'method_call_handler.dart'; +import 'spec/common.dart' show ErrorInfo, AblyException; +import 'spec/constants.dart'; /// instance of [StandardMethodCodec] with custom [MessageCodec] for /// exchanging Ably types with platform via platform channels @@ -22,7 +24,6 @@ final StreamsChannel streamsChannel = /// Initializing ably on platform side by invoking `register` platform method. /// Register will clear any stale instances on platform. -const _initializeTimeout = Duration(seconds: 5); Future _initializer; Future _initialize() async { @@ -30,9 +31,12 @@ Future _initialize() async { AblyMethodCallHandler(methodChannel); _initializer = methodChannel .invokeMethod(PlatformMethod.registerAbly) - .timeout(_initializeTimeout, onTimeout: () { + .timeout(Timeouts.initializeTimeout, onTimeout: () { _initializer = null; - throw TimeoutException('Initialization timed out.', _initializeTimeout); + throw TimeoutException( + 'Initialization timed out.', + Timeouts.initializeTimeout, + ); }); } return _initializer; @@ -45,5 +49,13 @@ Future _initialize() async { /// (as hot-restart is known to not clear any objects on platform side) Future invokePlatformMethod(String method, [Object arguments]) async { await _initialize(); - return methodChannel.invokeMethod(method, arguments); + try { + return await methodChannel.invokeMethod(method, arguments); + } on PlatformException catch (pe) { + if (pe.details is ErrorInfo) { + throw AblyException.fromPlatformException(pe); + } else { + rethrow; + } + } } diff --git a/lib/src/spec/common.dart b/lib/src/spec/common.dart index 085d5eb4f..7ebc4e404 100644 --- a/lib/src/spec/common.dart +++ b/lib/src/spec/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; import '../../ably_flutter.dart'; import '../impl/realtime/connection.dart'; @@ -517,6 +518,12 @@ class AblyException implements Exception { this.errorInfo, ]); + /// create AblyException from [PlatformException] + AblyException.fromPlatformException(PlatformException pe) + : code = pe.code, + message = pe.message, + errorInfo = pe.details as ErrorInfo; + @override String toString() { if (message == null) { @@ -901,17 +908,40 @@ class Stats { StatsRequestCount tokenRequests; } +/// Iterator class for [Channels.iterator] +class _ChannelIterator implements Iterator { + _ChannelIterator(this._channels); + + final List _channels; + + int _currentIndex = 0; + + T _currentChannel; + + @override + T get current => _currentChannel; + + @override + bool moveNext() { + if (_currentIndex == _channels.length) { + return false; + } + _currentChannel = _channels[_currentIndex++]; + return true; + } +} + /// A collection of Channel objects accessible /// through [Rest.channels] or [Realtime.channels] -abstract class Channels { +abstract class Channels extends Iterable { /// stores channel name vs instance of [ChannelType] final _channels = {}; /// creates a channel with provided name and options - ChannelType createChannel(String name, ChannelOptions options); - - /// returns all channels - Iterable get all => _channels.values; + /// + /// This is a private method to be overridden by implementation classes + @protected + ChannelType createChannel(String name); /// creates a channel with [name]. /// @@ -919,8 +949,9 @@ abstract class Channels { ChannelType get(String name) { //TODO add ChannelOptions as optional argument here, // and pass it on to createChannel + assert(name != null, 'Channel name cannot be null'); if (_channels[name] == null) { - _channels[name] = createChannel(name, null); + _channels[name] = createChannel(name); } return _channels[name]; } @@ -928,8 +959,15 @@ abstract class Channels { /// returns true if a channel exists [name] bool exists(String name) => _channels[name] != null; + /// Same as [get]. + ChannelType operator [](String name) => get(name); + + @override + Iterator get iterator => + _ChannelIterator(_channels.values.toList()); + /// releases channel with [name] void release(String name) { - throw UnimplementedError(); + _channels.remove(name); } } diff --git a/lib/src/spec/connection.dart b/lib/src/spec/connection.dart index 8b6c92845..3b22fdcae 100644 --- a/lib/src/spec/connection.dart +++ b/lib/src/spec/connection.dart @@ -9,7 +9,7 @@ abstract class ConnectionInterface /// current state of this connection /// /// https://docs.ably.io/client-lib-development-guide/features/#connection-states-operations - ConnectionState state; + ConnectionState get state; /// Error information associated with connection failure /// diff --git a/lib/src/spec/constants.dart b/lib/src/spec/constants.dart index e5ef70804..09f83bcac 100644 --- a/lib/src/spec/constants.dart +++ b/lib/src/spec/constants.dart @@ -41,6 +41,14 @@ class ErrorCodes { /// Static timeouts used inside the SDK class Timeouts { /// max time allowed for retrying an operation for auth failure - /// in case of usage of authCallback + /// in case of usage of authCallback static const retryOperationOnAuthFailure = Duration(seconds: 30); + + /// max time dart side will wait for platform side to respond with a + /// platform handle + static const acquireHandleTimeout = Duration(seconds: 5); + + /// max time dart side will wait for platform side to respond after + /// initializing an Ably instance on platform side + static const initializeTimeout = Duration(seconds: 5); } diff --git a/lib/src/spec/message.dart b/lib/src/spec/message.dart index 15a7062da..dc0457abc 100644 --- a/lib/src/spec/message.dart +++ b/lib/src/spec/message.dart @@ -49,9 +49,44 @@ class MessageData { } } +/// Handles supported message extras types, their encoding and decoding +class MessageExtras { + final T _extras; + + /// Only Map and List types are supported + MessageExtras(this._extras) : assert(T == Map); + + /// retrieve extras + T get extras => _extras; + + /// initializes [MessageExtras] with given value and validates + /// the data type, runtime + static MessageExtras fromValue(Object value) { + if (value == null) { + return null; + } + assert( + value is MessageExtras || value is Map, + 'Message extras must be either `Map`, or `null`.' + ' Does not support $value ("${value.runtimeType}")', + ); + if (value is MessageExtras) { + return value; + } else if (value is Map) { + return MessageExtras(value); + } else { + throw AssertionError( + 'Message extras must be either `Map`, or `null`.' + ' Does not support $value ("${value.runtimeType}")', + ); + } + } +} + /// An individual message to be sent/received by Ably /// /// https://docs.ably.io/client-lib-development-guide/features/#TM1 +@immutable class Message { /// A unique ID for this message /// @@ -88,8 +123,13 @@ class Message { /// https://docs.ably.io/client-lib-development-guide/features/#TM2g final String name; - /// Extras, if available - final Map extras; + final MessageExtras _extras; + + /// Message extras that may contain message metadata + /// and/or ancillary payloads + /// + /// https://docs.ably.io/client-lib-development-guide/features/#TM2i + Object get extras => _extras?.extras; /// Creates a message instance with [name], [data] and [clientId] Message({ @@ -100,8 +140,59 @@ class Message { this.connectionId, this.timestamp, this.encoding, - this.extras, - }) : _data = MessageData.fromValue(data); + Object extras, + }) : _data = MessageData.fromValue(data), + _extras = MessageExtras.fromValue(extras); + + @override + bool operator ==(Object other) => + other is Message && + other.id == id && + other.name == name && + other.data == data && + other.extras == extras && + other.encoding == encoding && + other.clientId == clientId && + other.timestamp == timestamp && + other.connectionId == connectionId; + + @override + int get hashCode => '$id:' + '$name:' + '$encoding:' + '$clientId:' + '$timestamp:' + '$connectionId:' + '${data?.toString()}:' + '${extras?.toString()}:' + .hashCode; + + /// https://docs.ably.io/client-lib-development-guide/features/#TM3 + /// + /// TODO(tiholic): decoding and decryption is not implemented as per + /// RSL6 and RLS6b as mentioned in TM3 + Message.fromEncoded( + Map jsonObject, [ + ChannelOptions channelOptions, + ]) : id = jsonObject['id'] as String, + name = jsonObject['name'] as String, + clientId = jsonObject['clientId'] as String, + connectionId = jsonObject['connectionId'] as String, + _data = MessageData.fromValue(jsonObject['data']), + encoding = jsonObject['encoding'] as String, + _extras = MessageExtras.fromValue(jsonObject['extras']), + timestamp = jsonObject['timestamp'] != null + ? DateTime.fromMillisecondsSinceEpoch( + jsonObject['timestamp'] as int, + ) + : null; + + /// https://docs.ably.io/client-lib-development-guide/features/#TM3 + static List fromEncodedArray( + List> jsonArray, [ + ChannelOptions channelOptions, + ]) => + jsonArray.map((e) => Message.fromEncoded(e, channelOptions)).toList(); @override String toString() => 'Message' @@ -151,8 +242,13 @@ class PresenceMessage { /// https://docs.ably.io/client-lib-development-guide/features/#TP3f final String encoding; + final MessageExtras _extras; + + /// Message extras that may contain message metadata + /// and/or ancillary payloads + /// /// https://docs.ably.io/client-lib-development-guide/features/#TP3i - final Map extras; + Object get extras => _extras?.extras; /// https://docs.ably.io/client-lib-development-guide/features/#TP3g final DateTime timestamp; @@ -168,9 +264,10 @@ class PresenceMessage { this.connectionId, Object data, this.encoding, - this.extras, + Object extras, this.timestamp, - }) : _data = MessageData.fromValue(data); + }) : _data = MessageData.fromValue(data), + _extras = MessageExtras.fromValue(extras); @override bool operator ==(Object other) => @@ -185,7 +282,15 @@ class PresenceMessage { other.timestamp == timestamp; @override - int get hashCode => '$id:$clientId:$connectionId:$timestamp'.hashCode; + int get hashCode => '$id:' + '$encoding:' + '$clientId:' + '$timestamp:' + '$connectionId:' + '${data?.toString()}:' + '${action.toString()}:' + '${extras?.toString()}:' + .hashCode; /// https://docs.ably.io/client-lib-development-guide/features/#TP4 /// @@ -201,11 +306,11 @@ class PresenceMessage { connectionId = jsonObject['connectionId'] as String, _data = MessageData.fromValue(jsonObject['data']), encoding = jsonObject['encoding'] as String, - extras = Map.castFrom( - jsonObject['extras'] as Map), + _extras = MessageExtras.fromValue(jsonObject['extras']), timestamp = jsonObject['timestamp'] != null ? DateTime.fromMillisecondsSinceEpoch( - jsonObject['timestamp'] as int) + jsonObject['timestamp'] as int, + ) : null; /// https://docs.ably.io/client-lib-development-guide/features/#TP4 @@ -213,7 +318,12 @@ class PresenceMessage { List> jsonArray, [ ChannelOptions channelOptions, ]) => - jsonArray.map((e) => PresenceMessage.fromEncoded(e)).toList(); + jsonArray + .map((jsonObject) => PresenceMessage.fromEncoded( + jsonObject, + channelOptions, + )) + .toList(); @override String toString() => 'PresenceMessage' diff --git a/lib/src/spec/realtime/channels.dart b/lib/src/spec/realtime/channels.dart index 039293eb4..9b2ce7adb 100644 --- a/lib/src/spec/realtime/channels.dart +++ b/lib/src/spec/realtime/channels.dart @@ -9,6 +9,21 @@ import '../rest/channels.dart'; import 'presence.dart'; import 'realtime.dart'; +/// options provided when instantiating a realtime channel +/// +/// https://docs.ably.io/client-lib-development-guide/features/#TB1 +class RealtimeChannelOptions extends ChannelOptions { + /// https://docs.ably.io/client-lib-development-guide/features/#TB2c + final Map params; + + /// https://docs.ably.io/client-lib-development-guide/features/#TB2d + final List modes; + + /// create channel options with a cipher, params and modes + RealtimeChannelOptions(Object cipher, {this.params, this.modes}) + : super(cipher); +} + /// A named channel through with realtime client can interact with ably service. /// /// The same channel can be interacted with relevant APIs via rest channel. @@ -17,21 +32,14 @@ import 'realtime.dart'; abstract class RealtimeChannelInterface extends EventEmitter { /// creates a Realtime channel instance - RealtimeChannelInterface( - this.realtime, - this.name, - this.options, - ); + RealtimeChannelInterface(this.realtime, this.name); /// realtime client instance - RealtimeInterface realtime; + final RealtimeInterface realtime; /// name of the channel final String name; - /// channel options - final ChannelOptions options; - /// will hold reason for failure of attaching to channel in such cases ErrorInfo errorReason; @@ -97,11 +105,11 @@ abstract class RealtimeChannelInterface List names, }); - /// takes a ChannelOptions object and sets or updates the + /// takes a [RealtimeChannelOptions]] object and sets or updates the /// stored channel options /// /// https://docs.ably.io/client-lib-development-guide/features/#RTL16 - Future setOptions(ChannelOptions options); + Future setOptions(RealtimeChannelOptions options); } /// A collection of realtime channel objects diff --git a/lib/src/spec/rest/channels.dart b/lib/src/spec/rest/channels.dart index 2eada5505..ac619fecc 100644 --- a/lib/src/spec/rest/channels.dart +++ b/lib/src/spec/rest/channels.dart @@ -6,10 +6,12 @@ import 'presence.dart'; /// options provided when instantiating a channel /// /// https://docs.ably.io/client-lib-development-guide/features/#TB1 -abstract class ChannelOptions { +class ChannelOptions { /// https://docs.ably.io/client-lib-development-guide/features/#TB2b - dynamic cipher; -// TODO add params and modes for realtime channel options + final Object cipher; + + /// create channel options with a cipher + ChannelOptions(this.cipher) : assert(cipher != null, 'cipher cannot be null'); } /// A named channel through with rest client can interact with ably service. @@ -19,11 +21,7 @@ abstract class ChannelOptions { /// https://docs.ably.io/client-lib-development-guide/features/#RSL1 abstract class RestChannelInterface { /// creates a Rest channel instance - RestChannelInterface( - this.rest, - this.name, - this.options, - ); + RestChannelInterface(this.rest, this.name); /// reference to Rest client RestInterface rest; @@ -31,9 +29,6 @@ abstract class RestChannelInterface { /// name of the channel String name; - /// options of the channel - ChannelOptions options; - /// presence interface for this channel /// /// can only query presence on the channel and presence history diff --git a/pubspec.lock b/pubspec.lock index d15469278..4ff70cbd4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,28 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "7.0.0" + version: "18.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.17" + version: "1.2.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.0.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" boolean_selector: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" clock: dependency: transitive description: @@ -77,28 +77,21 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.0" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.14.0" + version: "1.0.1" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.16.2" + version: "3.0.0" fake_async: dependency: "direct dev" description: @@ -106,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" flutter: dependency: "direct main" description: flutter @@ -122,35 +122,28 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.14.0+3" + version: "2.0.0" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.0" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "1.0.0" js: dependency: transitive description: @@ -164,7 +157,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.0" matcher: dependency: transitive description: @@ -185,35 +178,21 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.7" - node_interop: - dependency: transitive - description: - name: node_interop - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - node_io: - dependency: transitive - description: - name: node_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" + version: "1.0.0" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.12" + version: "2.0.0" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.0.0" path: dependency: transitive description: @@ -241,35 +220,35 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.0.0" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.1.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.8" + version: "1.0.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -295,7 +274,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -330,21 +309,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.5" + version: "1.16.7" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.3.0" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.15" + version: "0.3.18" typed_data: dependency: transitive description: @@ -365,35 +344,35 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "6.1.0+1" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "2.0.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.7.3" + version: "1.0.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.0" sdks: - dart: ">=2.12.0-0.0 <3.0.0" + dart: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index a1beb8be2..05e04bd5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: # Specifying a slightly older version minimum for the 'pure' Dart test package # because the flutter_test package needs an older version of test_api. - test: ^1.9.4 + test: ^1.16.7 flutter: diff --git a/test/models/message_test.dart b/test/models/message_test.dart new file mode 100644 index 000000000..314be58ff --- /dev/null +++ b/test/models/message_test.dart @@ -0,0 +1,206 @@ +import 'package:ably_flutter/ably_flutter.dart'; +import 'package:test/test.dart'; + +void main() { + const messageId = 'message-id'; + const name = 'message-name'; + const clientId = 'client-id'; + const connectionId = 'connection-id'; + const data = { + 'a': 'b', + 'c': [1, 2, 3] + }; + const encoding = 'msgpack'; + const extras = { + 'a': 'b', + 'c': [1, 2, 3] + }; + final timestamp = DateTime.now(); + + group('Message', () { + group('Behaves like a model', () { + final message = Message( + id: messageId, + name: name, + clientId: clientId, + connectionId: connectionId, + data: data, + encoding: encoding, + extras: extras, + timestamp: timestamp, + ); + + test('#id retrieves id', () { + expect(message.id, messageId); + }); + + test('#name retrieves name', () { + expect(message.name, name); + }); + + test('#clientId retrieves clientId', () { + expect(message.clientId, clientId); + }); + + test('#connectionId retrieves connectionId', () { + expect(message.connectionId, connectionId); + }); + + test('#data retrieves data', () { + expect(message.data, data); + }); + + test('#encoding retrieves encoding', () { + expect(message.encoding, encoding); + }); + + test('#extras retrieves extras', () { + expect(message.extras, extras); + }); + + test('#timestamp retrieves timestamp', () { + expect(message.timestamp, timestamp); + }); + + test('#== is true and hashes match when attributes are same', () { + final message2 = Message( + id: messageId, + name: name, + clientId: clientId, + connectionId: connectionId, + data: data, + encoding: encoding, + extras: extras, + timestamp: timestamp, + ); + expect(message == message2, true); + expect(message.hashCode == message2.hashCode, true); + }); + + test("#== is false and hashes don't match when attributes are not same", + () { + final message2 = Message( + id: messageId, + name: 'other-name', + clientId: clientId, + connectionId: connectionId, + data: data, + encoding: encoding, + extras: extras, + timestamp: timestamp, + ); + expect(message == message2, false); + expect(message.hashCode == message2.hashCode, false); + }); + + test("#== is false and hashes don't match for different classes", () { + final object = Object(); + expect(message == object, false); + expect(message.hashCode == object.hashCode, false); + }); + + group('fromEncoded', () { + test('returns a presence message object', () { + final message = Message.fromEncoded({ + 'id': messageId, + 'name': name, + 'clientId': clientId, + 'connectionId': connectionId, + 'data': data, + 'encoding': encoding, + 'extras': extras, + 'timestamp': timestamp.millisecondsSinceEpoch, + }); + expect(message.id, messageId); + expect(message.name, name); + expect(message.clientId, clientId); + expect(message.connectionId, connectionId); + expect(message.data, data); + expect(message.encoding, encoding); + expect(message.extras, extras); + expect( + message.timestamp, + DateTime.fromMillisecondsSinceEpoch( + timestamp.millisecondsSinceEpoch, + ), + ); + }); + }); + + group('fromEncodedArray', () { + test('returns a list of presence message objects', () { + final messages = Message.fromEncodedArray([ + { + 'id': messageId, + 'name': name, + 'clientId': clientId, + 'connectionId': connectionId, + 'data': data, + 'encoding': encoding, + 'extras': extras, + 'timestamp': timestamp.millisecondsSinceEpoch, + } + ]); + final message = messages[0]; + expect(message.id, messageId); + expect(message.name, name); + expect(message.clientId, clientId); + expect(message.connectionId, connectionId); + expect(message.data, data); + expect(message.encoding, encoding); + expect(message.extras, extras); + expect( + message.timestamp, + DateTime.fromMillisecondsSinceEpoch( + timestamp.millisecondsSinceEpoch, + ), + ); + }); + }); + + group('arguments with ', () { + test('null name, extras, client_id and encoding are allowed', () { + final message = Message(); + expect(message.name, null); + expect(message.encoding, null); + expect(message.clientId, null); + expect(message.extras, null); + }); + test('a map of extras is allowed', () { + final message = Message(extras: const {'key': 'value'}); + expect(message.extras, const {'key': 'value'}); + }); + test('a list of extras is not allowed', () { + expect( + () => Message(extras: const ['item1', 'item2']), + throwsA(isA()), + ); + }); + test('a string for extras is not allowed', () { + expect( + () => Message(extras: 'extra'), + throwsA(isA()), + ); + }); + test('an int for extras is not allowed', () { + expect( + () => Message(extras: 1), + throwsA(isA()), + ); + }); + test('a bool for extras is not allowed', () { + expect( + () => Message(extras: true), + throwsA(isA()), + ); + }); + test('any other Object for extras is not allowed', () { + expect( + () => Message(extras: Object()), + throwsA(isA()), + ); + }); + }); + }); + }); +} diff --git a/test/models/presence_message_test.dart b/test/models/presence_message_test.dart index 2f42cf258..b98a6b76c 100644 --- a/test/models/presence_message_test.dart +++ b/test/models/presence_message_test.dart @@ -28,28 +28,28 @@ void main() { extras: extras, timestamp: timestamp, ); - test('Retrieves id', () { + test('#id retrieves id', () { expect(presenceMessage.id, messageId); }); - test('Retrieves action', () { + test('#action retrieves action', () { expect(presenceMessage.action, action); }); - test('Retrieves clientId', () { + test('#clientId retrieves clientId', () { expect(presenceMessage.clientId, clientId); }); - test('Retrieves connectionId', () { + test('#connectionId retrieves connectionId', () { expect(presenceMessage.connectionId, connectionId); }); - test('Retrieves data', () { + test('#data retrieves data', () { expect(presenceMessage.data, data); }); - test('Retrieves encoding', () { + test('#encoding retrieves encoding', () { expect(presenceMessage.encoding, encoding); }); - test('Retrieves extras', () { + test('#extras retrieves extras', () { expect(presenceMessage.extras, extras); }); - test('Retrieves timestamp', () { + test('#timestamp retrieves timestamp', () { expect(presenceMessage.timestamp, timestamp); }); @@ -167,5 +167,49 @@ void main() { ); }); }); + + group('arguments with ', () { + test('null id, extras, client_id and encoding are allowed', () { + final message = PresenceMessage(); + expect(message.id, null); + expect(message.encoding, null); + expect(message.clientId, null); + expect(message.extras, null); + }); + test('a map of extras is allowed', () { + final message = PresenceMessage(extras: const {'key': 'value'}); + expect(message.extras, const {'key': 'value'}); + }); + test('a list of extras is not allowed', () { + expect( + () => PresenceMessage(extras: const ['item1', 'item2']), + throwsA(isA()), + ); + }); + test('a string for extras is not allowed', () { + expect( + () => PresenceMessage(extras: 'extra'), + throwsA(isA()), + ); + }); + test('an int for extras is not allowed', () { + expect( + () => PresenceMessage(extras: 1), + throwsA(isA()), + ); + }); + test('a bool for extras is not allowed', () { + expect( + () => PresenceMessage(extras: true), + throwsA(isA()), + ); + }); + test('any other Object for extras is not allowed', () { + expect( + () => PresenceMessage(extras: Object()), + throwsA(isA()), + ); + }); + }); }); } diff --git a/test/realtime/channel_test.dart b/test/realtime/channel_test.dart index 6dd8797e7..bb6a3b1ca 100644 --- a/test/realtime/channel_test.dart +++ b/test/realtime/channel_test.dart @@ -2,208 +2,170 @@ import 'dart:async'; import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter/src/impl/message.dart'; -import 'package:ably_flutter/src/method_call_handler.dart'; -import 'package:ably_flutter/src/platform.dart' as platform; import 'package:fake_async/fake_async.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pedantic/pedantic.dart'; -void main() { - final methodChannel = platform.methodChannel; +import '../utils.dart'; +void main() { TestWidgetsFlutterBinding.ensureInitialized(); - // Used to generate unique handle ids - int handleCounter; - - // Keep created channel instances associated with its handle. - final channels = {}; - - final publishedMessages = []; + MockMethodCallManager manager; setUp(() { - channels.clear(); - publishedMessages.clear(); - handleCounter = 0; - var isAuthenticated = false; - - methodChannel.setMockMethodCallHandler((methodCall) async { - switch (methodCall.method) { - case PlatformMethod.registerAbly: - return true; - - case PlatformMethod.createRealtimeWithOptions: - final handle = ++handleCounter; - channels[handle] = methodCall.arguments as AblyMessage; - return handle; - - case PlatformMethod.publishRealtimeChannelMessage: - final message = methodCall.arguments as AblyMessage; - final handle = (message.message as AblyMessage).handle; - final ablyChannel = channels[handle]; - final clientOptions = ablyChannel.message as ClientOptions; - - // `authUrl` is used to indicate the presence of an authCallback, - // because function references (in `authCallback`) get dropped by the - // PlatformChannel. - if (!isAuthenticated && clientOptions.authUrl == 'hasAuthCallback') { - await AblyMethodCallHandler(methodChannel).onRealtimeAuthCallback( - AblyMessage(TokenParams(timestamp: DateTime.now()), - handle: handle)); - isAuthenticated = true; - throw PlatformException( - code: ErrorCodes.authCallbackFailure.toString()); - } - - publishedMessages.add(message); - return null; - - default: - return throw Exception('Unexpected method call: ${methodCall.method}' - ' args: ${methodCall.arguments}'); - } - }); + manager = MockMethodCallManager(); + manager.methodChannel.setMockMethodCallHandler(manager.handler); }); tearDown(() { - methodChannel.setMockMethodCallHandler(null); - }); - - test('publish realtime message without authCallback', () async { - // setup - final realtime = Realtime(key: 'TEST-KEY'); - final channel = realtime.channels.get('test'); - - // exercise - await channel.publish(name: 'name', data: 'data1'); - await channel.publish(message: Message(name: 'name', data: 'data')); - await channel.publish(messages: [Message(name: 'name', data: 'data')]); - - // verification - expect(publishedMessages.length, 3); - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data1'); + manager.methodChannel.setMockMethodCallHandler(null); }); - test('publish message with authCallback', () async { - // setup - final authCallback = expectAsync1((token) async {}, max: 2); + group('realtime#channels#channel', () { + test('publish realtime message without authCallback', () async { + // setup + final realtime = Realtime(key: 'TEST-KEY'); + final channel = realtime.channels.get('test'); + + // exercise + await channel.publish(name: 'name', data: 'data1'); + await channel.publish(message: Message(name: 'name', data: 'data')); + await channel.publish(messages: [Message(name: 'name', data: 'data')]); + + // verification + expect(manager.publishedMessages.length, 3); + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data1'); + }); - final options = ClientOptions() - ..authCallback = authCallback - ..authUrl = 'hasAuthCallback'; - final realtime = Realtime(options: options, key: 'TEST-KEY'); + test('publish message with authCallback', () async { + // setup + final authCallback = expectAsync1((token) async {}, max: 2); - final channel = realtime.channels.get('test'); + final options = ClientOptions() + ..authCallback = authCallback + ..authUrl = 'hasAuthCallback'; + final realtime = Realtime(options: options, key: 'TEST-KEY'); - // exercise - await channel.publish(name: 'name', data: 'data2'); + final channel = realtime.channels.get('test'); - // verification + // exercise + await channel.publish(name: 'name', data: 'data2'); - expect(publishedMessages.length, 1); - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data2'); - }); + // verification - test('publish realtime message with authCallback timing out', () async { - // setup - final tooMuchDelay = - Timeouts.retryOperationOnAuthFailure + const Duration(seconds: 2); - var authCallbackCounter = 0; + expect(manager.publishedMessages.length, 1); + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data2'); + }); - Future timingOutOnceThenSucceedsAuthCallback(TokenParams token) { - if (authCallbackCounter == 0) { - authCallbackCounter++; - throw TimeoutException('Timed out'); + test('publish realtime message with authCallback timing out', () async { + // setup + final tooMuchDelay = + Timeouts.retryOperationOnAuthFailure + const Duration(seconds: 2); + var authCallbackCounter = 0; + + Future timingOutOnceThenSucceedsAuthCallback(TokenParams token) { + if (authCallbackCounter == 0) { + authCallbackCounter++; + throw TimeoutException('Timed out'); + } + return Future.value(); } - return Future.value(); - } - - unawaited( - fakeAsync((async) async { - final options = ClientOptions() - ..authCallback = timingOutOnceThenSucceedsAuthCallback - ..authUrl = 'hasAuthCallback'; - final realtime = Realtime(options: options, key: 'TEST-KEY'); - final channel = realtime.channels.get('test'); - - // exercise - final future1 = channel.publish(name: 'name', data: 'data3-1'); - final future2 = channel.publish(name: 'name', data: 'data3-2'); - - // verification - expect(future1, throwsA(isA())); - expect(future2, throwsA(isA())); - - async.elapse(tooMuchDelay); - - expect(publishedMessages.length, 0); - - // Send another message after timeout with authCallback succeeding - - // setup - // exercise - final future3 = channel.publish(name: 'name', data: 'data3-3'); - - // verification - async.elapse(Duration.zero); - await future3; - - expect(publishedMessages.length, 1); - - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data3-2'); - }), - ); - }); - test('publish 2 realtime messages with authCallback', () async { - // setup - final authCallback = expectAsync1((token) async {}); - - final options = ClientOptions() - ..authCallback = authCallback - ..authUrl = 'hasAuthCallback'; - final realtime = Realtime(options: options, key: 'TEST-KEY'); - final channel = realtime.channels.get('test'); - - // exercise - await channel.publish(name: 'name', data: 'data4'); - await channel.publish(name: 'name', data: 'data5'); - - // verification - expect(publishedMessages.length, 2); - final message0 = publishedMessages[0].message as AblyMessage; - final messageData0 = message0.message as Map; - expect(messageData0['channel'], 'test'); - expect(messageData0['messages'], isA()); - final messages = List.from(messageData0['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data4'); - - final message1 = publishedMessages[1].message as AblyMessage; - final messageData1 = message1.message as Map; - expect(messageData1['channel'], 'test'); - expect(messageData1['messages'], isA()); - final messages1 = List.from(messageData1['messages'] as List); - expect(messages1[0].name, 'name'); - expect(messages1[0].data, 'data5'); - }, timeout: Timeout.none); + unawaited( + fakeAsync((async) async { + final options = ClientOptions() + ..authCallback = timingOutOnceThenSucceedsAuthCallback + ..authUrl = 'hasAuthCallback'; + final realtime = Realtime(options: options, key: 'TEST-KEY'); + final channel = realtime.channels.get('test'); + + // exercise + final future1 = channel.publish(name: 'name', data: 'data3-1'); + final future2 = channel.publish(name: 'name', data: 'data3-2'); + + // verification + expect(future1, throwsA(isA())); + expect(future2, throwsA(isA())); + + async.elapse(tooMuchDelay); + + expect(manager.publishedMessages.length, 0); + + // Send another message after timeout with authCallback succeeding + + // setup + // exercise + final future3 = channel.publish(name: 'name', data: 'data3-3'); + + // verification + async.elapse(Duration.zero); + await future3; + + expect(manager.publishedMessages.length, 1); + + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data3-2'); + }), + ); + }); + + test('publish 2 realtime messages with authCallback', () async { + // setup + final authCallback = expectAsync1((token) async {}); + + final options = ClientOptions() + ..authCallback = authCallback + ..authUrl = 'hasAuthCallback'; + final realtime = Realtime(options: options, key: 'TEST-KEY'); + final channel = realtime.channels.get('test'); + + // exercise + await channel.publish(name: 'name', data: 'data4'); + await channel.publish(name: 'name', data: 'data5'); + + // verification + expect(manager.publishedMessages.length, 2); + final message0 = manager.publishedMessages[0].message as AblyMessage; + final messageData0 = message0.message as Map; + expect(messageData0[TxTransportKeys.channelName], 'test'); + expect(messageData0[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData0[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data4'); + + final message1 = manager.publishedMessages[1].message as AblyMessage; + final messageData1 = message1.message as Map; + expect(messageData1[TxTransportKeys.channelName], 'test'); + expect(messageData1[TxTransportKeys.messages], isA()); + final messages1 = + List.from(messageData1[TxTransportKeys.messages] as List); + expect(messages1[0].name, 'name'); + expect(messages1[0].data, 'data5'); + }, timeout: Timeout.none); + }); } diff --git a/test/rest/channel_test.dart b/test/rest/channel_test.dart index a51f9bce7..4ce9d69aa 100644 --- a/test/rest/channel_test.dart +++ b/test/rest/channel_test.dart @@ -2,211 +2,167 @@ import 'dart:async'; import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter/src/impl/message.dart'; -import 'package:ably_flutter/src/method_call_handler.dart'; -import 'package:ably_flutter/src/platform.dart' as platform; import 'package:fake_async/fake_async.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pedantic/pedantic.dart'; -void main() { - final methodChannel = platform.methodChannel; +import '../utils.dart'; +void main() { TestWidgetsFlutterBinding.ensureInitialized(); - // Used to generate unique handle ids - int handleCounter; - - // Keep created channel instances associated with its handle. - final channels = {}; - - final publishedMessages = []; + MockMethodCallManager manager; setUp(() { - channels.clear(); - publishedMessages.clear(); - handleCounter = 0; - var isAuthenticated = false; - - methodChannel.setMockMethodCallHandler((methodCall) async { - switch (methodCall.method) { - case PlatformMethod.registerAbly: - return true; - - case PlatformMethod.createRestWithOptions: - case PlatformMethod.createRealtimeWithOptions: - final handle = ++handleCounter; - channels[handle] = methodCall.arguments as AblyMessage; - return handle; - - case PlatformMethod.publish: - final message = methodCall.arguments as AblyMessage; - final handle = (message.message as AblyMessage).handle; - final ablyChannel = channels[handle]; - final clientOptions = ablyChannel.message as ClientOptions; - - // `authUrl` is used to indicate the presence of an authCallback, - // because function references (in `authCallback`) get dropped by the - // PlatformChannel. - if (!isAuthenticated && clientOptions.authUrl == 'hasAuthCallback') { - await AblyMethodCallHandler(methodChannel).onAuthCallback( - AblyMessage( - TokenParams(timestamp: DateTime.now()), - handle: handle, - ), - ); - isAuthenticated = true; - throw PlatformException( - code: ErrorCodes.authCallbackFailure.toString(), - ); - } - - publishedMessages.add(message); - return null; - - default: - return throw Exception('Unexpected method call: ${methodCall.method}' - ' args: ${methodCall.arguments}'); - } - }); + manager = MockMethodCallManager(); + manager.methodChannel.setMockMethodCallHandler(manager.handler); }); tearDown(() { - methodChannel.setMockMethodCallHandler(null); - }); - - test('publish message without authCallback', () async { - // setup - final rest = Rest(key: 'TEST-KEY'); - final channel = rest.channels.get('test'); - - // exercise - await channel.publish(name: 'name', data: 'data1'); - - // verification - expect(publishedMessages.length, 1); - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data1'); + manager.methodChannel.setMockMethodCallHandler(null); }); - test('publish message with authCallback', () async { - // setup - final authCallback = expectAsync1((token) async {}, max: 2); - - final options = ClientOptions() - ..authCallback = authCallback - ..authUrl = 'hasAuthCallback'; - final rest = Rest(options: options, key: 'TEST-KEY'); - - final channel = rest.channels.get('test'); - - // exercise - await channel.publish(name: 'name', data: 'data2'); + group('rest#channels#channel', () { + test('publishes message without authCallback', () async { + // setup + final rest = Rest(key: 'TEST-KEY'); + final channel = rest.channels.get('test'); + + // exercise + await channel.publish(name: 'name', data: 'data1'); + + // verification + expect(manager.publishedMessages.length, 1); + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data1'); + }); - // verification + test('publishes message with authCallback', () async { + // setup + final authCallback = expectAsync1((token) async {}, max: 2); + + final options = ClientOptions() + ..authCallback = authCallback + ..authUrl = 'hasAuthCallback'; + final rest = Rest(options: options, key: 'TEST-KEY'); + + final channel = rest.channels.get('test'); + + // exercise + await channel.publish(name: 'name', data: 'data2'); + + // verification + expect(manager.publishedMessages.length, 1); + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data2'); + }); - expect(publishedMessages.length, 1); - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data2'); - }); + test('publishes message with authCallback timing out', () async { + // setup + final tooMuchDelay = + Timeouts.retryOperationOnAuthFailure + const Duration(seconds: 2); + var authCallbackCounter = 0; + + Future timingOutOnceThenSucceedsAuthCallback(TokenParams token) { + if (authCallbackCounter == 0) { + authCallbackCounter++; + throw TimeoutException('Timed out'); + } + return Future.value(); + } - test('publish message with authCallback timing out', () async { - // setup - final tooMuchDelay = - Timeouts.retryOperationOnAuthFailure + const Duration(seconds: 2); - var authCallbackCounter = 0; + unawaited( + fakeAsync((async) async { + final options = ClientOptions() + ..authCallback = timingOutOnceThenSucceedsAuthCallback + ..authUrl = 'hasAuthCallback'; + final rest = Rest(options: options, key: 'TEST-KEY'); + final channel = rest.channels.get('test'); + + // exercise + final future1 = channel.publish(name: 'name', data: 'data3-1'); + final future2 = channel.publish(name: 'name', data: 'data3-2'); + + // verification + expect(future1, throwsA(isA())); + expect(future2, throwsA(isA())); + + async.elapse(tooMuchDelay); + + expect(manager.publishedMessages.length, 0); + + // Send another message after timeout with authCallback succeeding + + // setup + // exercise + final future3 = channel.publish(name: 'name', data: 'data3-3'); + + // verification + async.elapse(Duration.zero); + await future3; + + expect(manager.publishedMessages.length, 1); + + final firstMessage = + manager.publishedMessages.first.message as AblyMessage; + final messageData = firstMessage.message as Map; + expect(messageData[TxTransportKeys.channelName], 'test'); + expect(messageData[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data3-2'); + }), + ); + }); - Future timingOutOnceThenSucceedsAuthCallback(TokenParams token) { - if (authCallbackCounter == 0) { - authCallbackCounter++; - throw TimeoutException('Timed out'); - } - return Future.value(); - } - - unawaited( - fakeAsync((async) async { - final options = ClientOptions() - ..authCallback = timingOutOnceThenSucceedsAuthCallback - ..authUrl = 'hasAuthCallback'; - final rest = Rest(options: options, key: 'TEST-KEY'); - final channel = rest.channels.get('test'); - - // exercise - final future1 = channel.publish(name: 'name', data: 'data3-1'); - final future2 = channel.publish(name: 'name', data: 'data3-2'); - - // verification - expect(future1, throwsA(isA())); - expect(future2, throwsA(isA())); - - async.elapse(tooMuchDelay); - - expect(publishedMessages.length, 0); - - // Send another message after timeout with authCallback succeeding - - // setup - // exercise - final future3 = channel.publish(name: 'name', data: 'data3-3'); - - // verification - async.elapse(Duration.zero); - await future3; - - expect(publishedMessages.length, 1); - - final firstMessage = publishedMessages.first.message as AblyMessage; - final messageData = firstMessage.message as Map; - expect(messageData['channel'], 'test'); - expect(messageData['messages'], isA()); - final messages = List.from(messageData['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data3-2'); - }), - ); + test('publishes another message with authCallback', () async { + // setup + final authCallback = expectAsync1((token) async {}); + + final options = ClientOptions() + ..authCallback = authCallback + ..authUrl = 'hasAuthCallback'; + final rest = Rest(options: options, key: 'TEST-KEY'); + final channel = rest.channels.get('test'); + + // exercise + await channel.publish(name: 'name', data: 'data4'); + await channel.publish(name: 'name', data: 'data5'); + + // verification + expect(manager.publishedMessages.length, 2); + final message0 = manager.publishedMessages[0].message as AblyMessage; + final messageData0 = message0.message as Map; + expect(messageData0[TxTransportKeys.channelName], 'test'); + expect(messageData0[TxTransportKeys.messages], isA()); + final messages = + List.from(messageData0[TxTransportKeys.messages] as List); + expect(messages[0].name, 'name'); + expect(messages[0].data, 'data4'); + + final message1 = manager.publishedMessages[1].message as AblyMessage; + final messageData1 = message1.message as Map; + expect(messageData1[TxTransportKeys.channelName], 'test'); + expect(messageData1[TxTransportKeys.messages], isA()); + final messages2 = + List.from(messageData1[TxTransportKeys.messages] as List); + expect(messages2[0].name, 'name'); + expect(messages2[0].data, 'data5'); + }, timeout: Timeout.none); }); - - test('publish 2 message with authCallback', () async { - // setup - final authCallback = expectAsync1((token) async {}); - - final options = ClientOptions() - ..authCallback = authCallback - ..authUrl = 'hasAuthCallback'; - final rest = Rest(options: options, key: 'TEST-KEY'); - final channel = rest.channels.get('test'); - - // exercise - await channel.publish(name: 'name', data: 'data4'); - await channel.publish(name: 'name', data: 'data5'); - - // verification - expect(publishedMessages.length, 2); - final message0 = publishedMessages[0].message as AblyMessage; - final messageData0 = message0.message as Map; - expect(messageData0['channel'], 'test'); - expect(messageData0['messages'], isA()); - final messages = List.from(messageData0['messages'] as List); - expect(messages[0].name, 'name'); - expect(messages[0].data, 'data4'); - - final message1 = publishedMessages[1].message as AblyMessage; - final messageData1 = message1.message as Map; - expect(messageData1['channel'], 'test'); - expect(messageData1['messages'], isA()); - final messages2 = List.from(messageData1['messages'] as List); - expect(messages2[0].name, 'name'); - expect(messages2[0].data, 'data5'); - }, timeout: Timeout.none); } diff --git a/test/rest/channels_test.dart b/test/rest/channels_test.dart new file mode 100644 index 000000000..e83d3a509 --- /dev/null +++ b/test/rest/channels_test.dart @@ -0,0 +1,114 @@ +import 'package:ably_flutter/ably_flutter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MockMethodCallManager manager; + RestPlatformChannels channels; + + setUp(() { + manager = MockMethodCallManager(); + final rest = Rest(key: 'TEST-KEY'); + channels = rest.channels; + }); + + tearDown(() { + manager.reset(); + }); + + group('rest#channels', () { + test('creates channel with #get', () { + final channel = channels.get('channel-1'); + expect(channel.name, 'channel-1'); + }); + + test('checks if channel exist with #exists', () { + var hasChannel = channels.exists('channel-2'); + expect(hasChannel, false); + + final channel = channels.get('channel-2'); + expect(channel.name, 'channel-2'); + + hasChannel = channels.exists('channel-2'); + expect(hasChannel, true); + }); + + test('creates/returns channel with list accessor #[]', () { + final channel2 = channels.get('channel-2'); + final chanel2WithSyntacticSugar = channels['channel-2']; + expect(chanel2WithSyntacticSugar, channel2); + + final channel3 = channels['channel-3']; + expect(channel3.name, 'channel-3'); + }); + + test('iterates over created channels', () { + channels + ..get('channel-1') + ..get('channel-2') + ..get('channel-3') + ..get('channel-33'); + + expect( + channels.map((e) => e.name).toList(), + orderedEquals(const [ + 'channel-1', + 'channel-2', + 'channel-3', + 'channel-33', + ]), + ); + + expect( + channels.where((e) => e.name.endsWith('3')).map((e) => e.name).toList(), + orderedEquals(const [ + 'channel-3', + 'channel-33', + ]), + ); + }); + + test('allows querying length', () { + channels..get('channel-1')..get('channel-2'); + + expect(channels.length, 2); + + channels..get('channel-3')..get('channel-33'); + + expect(channels.length, 4); + }); + + group('#release', () { + test( + '(RSN4a) Takes one argument, the channel name,' + ' and releases the corresponding channel entity', () { + channels..get('channel-1')..get('channel-2'); + expect(channels.length, 2); + channels.release('channel-1'); + expect(channels.length, 1); + expect(channels.exists('channel-1'), false); + }); + + test( + '(RSN4b) Calling release() with a channel name' + ' that does not correspond to an extant channel' + ' entity must return without error', () { + channels..get('channel-1')..get('channel-2'); + expect(channels.length, 2); + channels.release('channel-3'); + expect(channels.length, 2); + expect(channels.exists('channel-1'), true); + expect(channels.exists('channel-2'), true); + }); + }); + }); + + group('rest#channels#channel', () { + test('Allows only non-null String input for name', () { + expect(() => channels.get(null), throwsA(isA())); + }); + }); +} diff --git a/test/utils.dart b/test/utils.dart new file mode 100644 index 000000000..94dc0bd64 --- /dev/null +++ b/test/utils.dart @@ -0,0 +1,96 @@ +import 'package:ably_flutter/ably_flutter.dart'; +import 'package:ably_flutter/src/impl/message.dart'; +import 'package:ably_flutter/src/method_call_handler.dart'; +import 'package:ably_flutter/src/platform.dart' as platform; +import 'package:flutter/services.dart'; + +typedef MethodCallHandler = Future Function(MethodCall); + +class MockMethodCallManager { + int handleCounter = 0; + bool isAuthenticated = false; + final channels = {}; + final publishedMessages = []; + final methodChannel = platform.methodChannel; + + MockMethodCallManager() { + methodChannel.setMockMethodCallHandler(handler); + } + + void reset() { + channels.clear(); + publishedMessages.clear(); + handleCounter = 0; + methodChannel.setMockMethodCallHandler(null); + } + + Future handler(MethodCall methodCall) async { + switch (methodCall.method) { + case PlatformMethod.registerAbly: + return true; + + case PlatformMethod.createRestWithOptions: + case PlatformMethod.createRealtimeWithOptions: + final handle = ++handleCounter; + channels[handle] = methodCall.arguments as AblyMessage; + return handle; + + case PlatformMethod.publish: + final message = methodCall.arguments as AblyMessage; + final handle = (message.message as AblyMessage).handle; + final ablyChannel = channels[handle]; + final clientOptions = ablyChannel.message as ClientOptions; + + // `authUrl` is used to indicate the presence of an authCallback, + // because function references (in `authCallback`) get dropped by the + // PlatformChannel. + if (!isAuthenticated && clientOptions.authUrl == 'hasAuthCallback') { + await AblyMethodCallHandler(methodChannel).onAuthCallback( + AblyMessage( + TokenParams(timestamp: DateTime.now()), + handle: handle, + ), + ); + isAuthenticated = true; + throw PlatformException( + code: ErrorCodes.authCallbackFailure.toString(), + details: ErrorInfo(), + ); + } + + publishedMessages.add(message); + return null; + + case PlatformMethod.publishRealtimeChannelMessage: + final message = methodCall.arguments as AblyMessage; + final handle = (message.message as AblyMessage).handle; + final ablyChannel = channels[handle]; + final clientOptions = ablyChannel.message as ClientOptions; + + // `authUrl` is used to indicate the presence of an authCallback, + // because function references (in `authCallback`) get dropped by the + // PlatformChannel. + if (!isAuthenticated && clientOptions.authUrl == 'hasAuthCallback') { + await AblyMethodCallHandler(methodChannel).onRealtimeAuthCallback( + AblyMessage(TokenParams(timestamp: DateTime.now()), + handle: handle)); + isAuthenticated = true; + throw PlatformException( + code: ErrorCodes.authCallbackFailure.toString(), + details: ErrorInfo(), + ); + } + + publishedMessages.add(message); + return null; + + case PlatformMethod.releaseRestChannel: + case PlatformMethod.releaseRealtimeChannel: + return null; + + default: + return throw Exception('Unexpected method call: ${methodCall.method}' + ' args: ${methodCall.arguments}'); + } + } +} diff --git a/test_integration/ios/Podfile.lock b/test_integration/ios/Podfile.lock index 01cc46466..c06968217 100644 --- a/test_integration/ios/Podfile.lock +++ b/test_integration/ios/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - Ably (1.2.3): + - Ably (1.2.4): - AblyDeltaCodec (= 1.2.0) - msgpack (= 0.3.1) - - SocketRocketAblyFork (= 0.5.2-ably-7) + - SocketRocketAblyFork (= 0.5.2-ably-8) - ULID (= 1.1.0) - ably_flutter (0.0.5): - - Ably + - Ably (= 1.2.4) - Flutter - AblyDeltaCodec (1.2.0) - Flutter (1.0.0) - msgpack (0.3.1) - - SocketRocketAblyFork (0.5.2-ably-7) + - SocketRocketAblyFork (0.5.2-ably-8) - ULID (1.1.0) DEPENDENCIES: @@ -32,14 +32,14 @@ EXTERNAL SOURCES: :path: Flutter SPEC CHECKSUMS: - Ably: 9f143677993cb6df20fce4551b9b6b2e55ad2f4d - ably_flutter: a35c3926509e76b0fc69b9199872b9d428882611 + Ably: 863d35bb6a6aa5fc537304ef19f85cfa51d1bbde + ably_flutter: b37439c7b63f4b3e44f4674c5bfb56452d193ad0 AblyDeltaCodec: 6123f31df5b04a0f5452968505a46ba16a9eb689 Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c msgpack: a14de9216d29cfd0a7aff5af5150601a27e899a4 - SocketRocketAblyFork: 33ff506ecb565498051b8f358d60ae44274c3981 + SocketRocketAblyFork: e6e3d820bf8ff9e6265d2af81b9f11f270351ea6 ULID: b4714891a02819364faecd574a53e391c4c6de9d PODFILE CHECKSUM: 7d251af3d33bd11d1f2d86f8fb2d423bf85567b7 -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 diff --git a/test_integration/ios/Runner.xcodeproj/project.pbxproj b/test_integration/ios/Runner.xcodeproj/project.pbxproj index f8849d6e6..174f1fcba 100644 --- a/test_integration/ios/Runner.xcodeproj/project.pbxproj +++ b/test_integration/ios/Runner.xcodeproj/project.pbxproj @@ -231,23 +231,12 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Ably/Ably.framework", - "${BUILT_PRODUCTS_DIR}/AblyDeltaCodec/AblyDeltaCodec.framework", - "${BUILT_PRODUCTS_DIR}/SocketRocketAblyFork/SocketRocketAblyFork.framework", - "${BUILT_PRODUCTS_DIR}/ULID/ULID.framework", - "${BUILT_PRODUCTS_DIR}/ably_flutter/ably_flutter.framework", - "${BUILT_PRODUCTS_DIR}/msgpack/msgpack.framework", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ably.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AblyDeltaCodec.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocketAblyFork.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ULID.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ably_flutter.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/msgpack.framework", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/test_integration/lib/config/data.dart b/test_integration/lib/config/data.dart deleted file mode 100644 index a1fcdec82..000000000 --- a/test_integration/lib/config/data.dart +++ /dev/null @@ -1,29 +0,0 @@ -final messagesToPublish = [ - [null, null], //name and message are both null - [null, 'Ably'], //name is null - ['name1', null], //message is null - ['name1', 'Ably'], //message is a string - [ - 'name2', - [1, 2, 3] - ], //message is a numeric list - [ - 'name2', - ['hello', 'ably'] - ], //message is a string list - [ - 'name3', - { - 'hello': 'ably', - 'items': ['1', 2.2, true] - } - ], //message is a map - [ - 'name3', - [ - {'hello': 'ably'}, - 'ably', - 'realtime' - ] - ] //message is a complex list -]; diff --git a/test_integration/lib/config/test_config.dart b/test_integration/lib/config/test_config.dart new file mode 100644 index 000000000..4efbf29c1 --- /dev/null +++ b/test_integration/lib/config/test_config.dart @@ -0,0 +1,3 @@ +class TestConstants { + static const Duration publishToHistoryDelay = Duration(seconds: 2); +} diff --git a/test_integration/lib/config/test_factory.dart b/test_integration/lib/config/test_factory.dart index 9383771cc..f1dd3e55a 100644 --- a/test_integration/lib/config/test_factory.dart +++ b/test_integration/lib/config/test_factory.dart @@ -1,21 +1,21 @@ import '../factory/reporter.dart'; -import '../test/app_key_provision_test.dart'; -import '../test/platform_and_ably_version_test.dart'; -import '../test/realtime_events_test.dart'; -import '../test/realtime_history_test.dart'; -import '../test/realtime_presence_enter_update_leave.dart'; -import '../test/realtime_presence_get.dart'; -import '../test/realtime_presence_history_test.dart'; -import '../test/realtime_presence_subscribe.dart'; -import '../test/realtime_publish_test.dart'; -import '../test/realtime_publish_with_auth_callback_test.dart'; -import '../test/realtime_subscribe.dart'; -import '../test/rest_history_test.dart'; -import '../test/rest_presence_get_test.dart'; -import '../test/rest_presence_history_test.dart'; -import '../test/rest_publish_test.dart'; -import '../test/rest_publish_with_auth_callback_test.dart'; -import '../test/test_helper_unhandled_exception_test.dart'; +import '../test/basic_test.dart'; +import '../test/helpers_test.dart'; +import '../test/realtime/realtime_events_test.dart'; +import '../test/realtime/realtime_history_test.dart'; +import '../test/realtime/realtime_presence_enter_update_leave.dart'; +import '../test/realtime/realtime_presence_get.dart'; +import '../test/realtime/realtime_presence_history_test.dart'; +import '../test/realtime/realtime_presence_subscribe.dart'; +import '../test/realtime/realtime_publish_test.dart'; +import '../test/realtime/realtime_publish_with_auth_callback_test.dart'; +import '../test/realtime/realtime_subscribe.dart'; +import '../test/rest/rest_capability_test.dart'; +import '../test/rest/rest_history_test.dart'; +import '../test/rest/rest_presence_get_test.dart'; +import '../test/rest/rest_presence_history_test.dart'; +import '../test/rest/rest_publish_test.dart'; +import '../test/rest/rest_publish_with_auth_callback_test.dart'; import 'test_names.dart'; typedef TestFactory = Future> Function({ @@ -29,6 +29,8 @@ final testFactory = { TestName.appKeyProvisioning: testAppKeyProvision, // rest tests TestName.restPublish: testRestPublish, + TestName.restPublishSpec: testRestPublishSpec, + TestName.restCapabilities: testRestCapabilities, TestName.restHistory: testRestHistory, TestName.restPublishWithAuthCallback: testRestPublishWithAuthCallback, TestName.restPresenceGet: testRestPresenceGet, @@ -45,5 +47,5 @@ final testFactory = { testRealtimePresenceEnterUpdateLeave, TestName.realtimePresenceSubscribe: testRealtimePresenceSubscribe, // helper tests - TestName.testHelperUnhandledExceptionTest: testTestHelperUnhandledException, + TestName.testHelperUnhandledExceptionTest: testHelperUnhandledException, }; diff --git a/test_integration/lib/config/test_names.dart b/test_integration/lib/config/test_names.dart index 7a82fb292..28d5ac112 100644 --- a/test_integration/lib/config/test_names.dart +++ b/test_integration/lib/config/test_names.dart @@ -11,6 +11,8 @@ class TestName { 'testHelperUnhandledExceptionTest'; static const String restPublish = 'restPublish'; + static const String restPublishSpec = 'restPublishSpec'; + static const String restCapabilities = 'restCapabilities'; static const String restHistory = 'restHistory'; static const String restPublishWithAuthCallback = 'restPublishWithAuthCallback'; diff --git a/test_integration/lib/test/app_key_provision_test.dart b/test_integration/lib/test/app_key_provision_test.dart deleted file mode 100644 index 1c7c44c11..000000000 --- a/test_integration/lib/test/app_key_provision_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:ably_flutter_example/provisioning.dart'; - -import '../factory/reporter.dart'; - -Future> testAppKeyProvision({ - Reporter reporter, - Map payload, -}) async => - { - 'appKey': (await provision('sandbox-')).toString(), - 'tokenRequest': await getTokenRequest(), - }; diff --git a/test_integration/lib/test/platform_and_ably_version_test.dart b/test_integration/lib/test/basic_test.dart similarity index 58% rename from test_integration/lib/test/platform_and_ably_version_test.dart rename to test_integration/lib/test/basic_test.dart index 9850b2d43..5f2d9c1aa 100644 --- a/test_integration/lib/test/platform_and_ably_version_test.dart +++ b/test_integration/lib/test/basic_test.dart @@ -1,6 +1,17 @@ import 'package:ably_flutter/ably_flutter.dart' as ably; +import 'package:ably_flutter_example/provisioning.dart'; + import '../factory/reporter.dart'; +Future> testAppKeyProvision({ + Reporter reporter, + Map payload, +}) async => + { + 'appKey': (await provision('sandbox-')).toString(), + 'tokenRequest': await getTokenRequest(), + }; + Future> testPlatformAndAblyVersion({ Reporter reporter, Map payload, diff --git a/test_integration/lib/test/test_helper_unhandled_exception_test.dart b/test_integration/lib/test/helpers_test.dart similarity index 71% rename from test_integration/lib/test/test_helper_unhandled_exception_test.dart rename to test_integration/lib/test/helpers_test.dart index 7bf0cd720..df2925842 100644 --- a/test_integration/lib/test/test_helper_unhandled_exception_test.dart +++ b/test_integration/lib/test/helpers_test.dart @@ -1,6 +1,6 @@ import '../factory/reporter.dart'; -Future> testTestHelperUnhandledException({ +Future> testHelperUnhandledException({ Reporter reporter, Map payload, }) async { diff --git a/test_integration/lib/test/realtime_events_test.dart b/test_integration/lib/test/realtime/realtime_events_test.dart similarity index 94% rename from test_integration/lib/test/realtime_events_test.dart rename to test_integration/lib/test/realtime/realtime_events_test.dart index cada67b4d..4967eaa05 100644 --- a/test_integration/lib/test/realtime_events_test.dart +++ b/test_integration/lib/test/realtime/realtime_events_test.dart @@ -1,8 +1,9 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/encoders.dart'; Future> testRealtimeEvents({ Reporter reporter, @@ -71,7 +72,7 @@ Future> testRealtimeEvents({ await realtime.close(); await Future.delayed(Duration.zero); while (realtime.connection.state != ConnectionState.closed) { - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); } recordChannelState(); // channel: detached recordConnectionState(); // connection: closed diff --git a/test_integration/lib/test/realtime_history_test.dart b/test_integration/lib/test/realtime/realtime_history_test.dart similarity index 51% rename from test_integration/lib/test/realtime_history_test.dart rename to test_integration/lib/test/realtime/realtime_history_test.dart index c1c4ef626..7a53908a0 100644 --- a/test_integration/lib/test/realtime_history_test.dart +++ b/test_integration/lib/test/realtime/realtime_history_test.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; -import 'realtime_publish_test.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/encoders.dart'; +import '../../utils/realtime.dart'; Future> testRealtimeHistory({ Reporter reporter, @@ -22,43 +23,53 @@ Future> testRealtimeHistory({ ({msg, exception}) => logMessages.add([msg, exception.toString()]), ); final channel = realtime.channels.get('test'); - await realtimeMessagesPublishUtil(channel); + await publishMessages(channel); final paginatedResult = await channel.history(); - final historyDefault = await _history(channel); - await Future.delayed(const Duration(seconds: 2)); + final historyDefault = await getHistory(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit4 = - await _history(channel, RealtimeHistoryParams(limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit4 = await getHistory( + channel, + RealtimeHistoryParams(limit: 4), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit2 = - await _history(channel, RealtimeHistoryParams(limit: 2)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit2 = await getHistory( + channel, + RealtimeHistoryParams(limit: 2), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyForwardLimit4 = await _history( - channel, RealtimeHistoryParams(direction: 'forwards', limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final historyForwardLimit4 = await getHistory( + channel, + RealtimeHistoryParams(direction: 'forwards', limit: 4), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); final time1 = DateTime.now(); //TODO(tiholic) iOS fails without this delay // - timestamp on message retrieved from history // is earlier than expected when ran in CI - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await channel.publish(name: 'history', data: 'test'); //TODO(tiholic) understand why tests fail without this delay - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time2 = DateTime.now(); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await channel.publish(name: 'history', data: 'test2'); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyWithStart = - await _history(channel, RealtimeHistoryParams(start: time1)); - final historyWithStartAndEnd = - await _history(channel, RealtimeHistoryParams(start: time1, end: time2)); - final historyAll = await _history(channel); + final historyWithStart = await getHistory( + channel, + RealtimeHistoryParams(start: time1), + ); + final historyWithStartAndEnd = await getHistory( + channel, + RealtimeHistoryParams(start: time1, end: time2), + ); + final historyAll = await getHistory(channel); return { 'handle': await realtime.handle, 'paginatedResult': encodePaginatedResult(paginatedResult, encodeMessage), @@ -74,16 +85,3 @@ Future> testRealtimeHistory({ 'log': logMessages, }; } - -Future>> _history( - RealtimeChannel channel, [ - RealtimeHistoryParams params, -]) async { - var results = await channel.history(params); - final messages = encodeList(results.items, encodeMessage); - while (results.hasNext()) { - results = await results.next(); - messages.addAll(encodeList(results.items, encodeMessage)); - } - return messages; -} diff --git a/test_integration/lib/test/realtime_presence_enter_update_leave.dart b/test_integration/lib/test/realtime/realtime_presence_enter_update_leave.dart similarity index 97% rename from test_integration/lib/test/realtime_presence_enter_update_leave.dart rename to test_integration/lib/test/realtime/realtime_presence_enter_update_leave.dart index e4d2813ae..5f8e2d576 100644 --- a/test_integration/lib/test/realtime_presence_enter_update_leave.dart +++ b/test_integration/lib/test/realtime/realtime_presence_enter_update_leave.dart @@ -2,8 +2,8 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../factory/reporter.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; final logMessages = >[]; @@ -95,6 +95,7 @@ Future> testRealtimePresenceEnterUpdateLeave({ } } + // Interact with different types of data. final actionMatrix = >[]; final realtimePresence = Realtime(options: getClientOptions(appKey, 'test-client')) diff --git a/test_integration/lib/test/realtime_presence_get.dart b/test_integration/lib/test/realtime/realtime_presence_get.dart similarity index 68% rename from test_integration/lib/test/realtime_presence_get.dart rename to test_integration/lib/test/realtime/realtime_presence_get.dart index d038085f0..be56f425f 100644 --- a/test_integration/lib/test/realtime_presence_get.dart +++ b/test_integration/lib/test/realtime/realtime_presence_get.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/realtime.dart'; final logMessages = >[]; @@ -27,7 +28,7 @@ Future> testRealtimePresenceGet({ final realtime = Realtime(options: getClientOptions(appKey)); final channel = realtime.channels.get('test'); - final membersInitial = await _members(channel); + final membersInitial = await getPresenceMembers(channel); // enter multiple clients for (var i = 0; i < messagesToPublish.length; i++) { @@ -36,25 +37,25 @@ Future> testRealtimePresenceGet({ ).channels.get('test').presence.enter(messagesToPublish[i][1]); } - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersDefault = await _members(channel); - await Future.delayed(const Duration(seconds: 2)); + final membersDefault = await getPresenceMembers(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersClientId = await _members( + final membersClientId = await getPresenceMembers( channel, RealtimePresenceParams(clientId: 'client-1'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); // TODO(tiholic) extract connection ID from realtime instance // after implementing `id` update on connection object from platform // Until then, `membersConnectionId` will be empty list - final membersConnectionId = await _members( + final membersConnectionId = await getPresenceMembers( channel, RealtimePresenceParams(connectionId: 'connection-1'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); return { 'handle': await realtime.handle, @@ -65,12 +66,3 @@ Future> testRealtimePresenceGet({ 'log': logMessages, }; } - -Future>> _members( - RealtimeChannel channel, [ - RealtimePresenceParams params, -]) async => - encodeList( - await channel.presence.get(params), - encodePresenceMessage, - ); diff --git a/test_integration/lib/test/realtime_presence_history_test.dart b/test_integration/lib/test/realtime/realtime_presence_history_test.dart similarity index 58% rename from test_integration/lib/test/realtime_presence_history_test.dart rename to test_integration/lib/test/realtime/realtime_presence_history_test.dart index c753967ce..11fd3601a 100644 --- a/test_integration/lib/test/realtime_presence_history_test.dart +++ b/test_integration/lib/test/realtime/realtime_presence_history_test.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/realtime.dart'; Future> testRealtimePresenceHistory({ Reporter reporter, @@ -23,7 +24,7 @@ Future> testRealtimePresenceHistory({ final realtime = Realtime(options: options); final channel = realtime.channels.get('test'); - final historyInitial = await _history(channel); + final historyInitial = await getPresenceHistory(channel); // creating presence history on channel final realtimePresence = channel.presence; @@ -36,44 +37,50 @@ Future> testRealtimePresenceHistory({ // leaves channel await realtimePresence.leave(messagesToPublish.last[1]); - final historyDefault = await _history(channel); - await Future.delayed(const Duration(seconds: 2)); + final historyDefault = await getPresenceHistory(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit4 = - await _history(channel, RealtimeHistoryParams(limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit4 = await getPresenceHistory( + channel, + RealtimeHistoryParams(limit: 4), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit2 = - await _history(channel, RealtimeHistoryParams(limit: 2)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit2 = await getPresenceHistory( + channel, + RealtimeHistoryParams(limit: 2), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyForwards = await _history( + final historyForwards = await getPresenceHistory( channel, RealtimeHistoryParams(direction: 'forwards'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time1 = DateTime.now(); //TODO(tiholic) iOS fails without this delay // - timestamp on message retrieved from history // is earlier than expected when ran in CI - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await realtimePresence.enter('enter-start-time'); // TODO(tiholic) understand why tests fail without this delay - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time2 = DateTime.now(); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await realtimePresence.leave('leave-end-time'); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyWithStart = - await _history(channel, RealtimeHistoryParams(start: time1)); - final historyWithStartAndEnd = await _history( + final historyWithStart = await getPresenceHistory( + channel, + RealtimeHistoryParams(start: time1), + ); + final historyWithStartAndEnd = await getPresenceHistory( channel, RealtimeHistoryParams(start: time1, end: time2), ); - final historyAll = await _history(channel); + final historyAll = await getPresenceHistory(channel); return { 'handle': await realtime.handle, @@ -88,19 +95,3 @@ Future> testRealtimePresenceHistory({ 'log': logMessages, }; } - -Future>> _history( - RealtimeChannel channel, [ - RealtimeHistoryParams params, -]) async { - var results = await channel.presence.history(params); - final messages = - encodeList(results.items, encodePresenceMessage); - while (results.hasNext()) { - results = await results.next(); - messages.addAll( - encodeList(results.items, encodePresenceMessage), - ); - } - return messages; -} diff --git a/test_integration/lib/test/realtime_presence_subscribe.dart b/test_integration/lib/test/realtime/realtime_presence_subscribe.dart similarity index 80% rename from test_integration/lib/test/realtime_presence_subscribe.dart rename to test_integration/lib/test/realtime/realtime_presence_subscribe.dart index 057656d9a..2a3b94f35 100644 --- a/test_integration/lib/test/realtime_presence_subscribe.dart +++ b/test_integration/lib/test/realtime/realtime_presence_subscribe.dart @@ -3,12 +3,16 @@ import 'dart:async'; import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/encoders.dart'; final logMessages = >[]; +List> _encode(List messages) => + encodeList(messages, encodePresenceMessage); + Future> testRealtimePresenceSubscribe({ Reporter reporter, Map payload, @@ -52,26 +56,23 @@ Future> testRealtimePresenceSubscribe({ // Wait for the update event as it is asynchronously triggered. // Then cancelling partial subscription expecting it to not receive // further presence events. - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await partialMessagesSubscription.cancel(); await presence.leave(messagesToPublish.last[1]); // Wait for the leave event to be received by listeners. // Assuming, they'd turn out in 2 seconds. - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await allMessagesSubscription.cancel(); await enterMessagesSubscription.cancel(); await enterUpdateMessagesSubscription.cancel(); return { - 'allMessages': encode(allMessages), - 'enterMessages': encode(enterMessages), - 'enterUpdateMessages': encode(enterUpdateMessages), - 'partialMessages': encode(partialMessages), + 'allMessages': _encode(allMessages), + 'enterMessages': _encode(enterMessages), + 'enterUpdateMessages': _encode(enterUpdateMessages), + 'partialMessages': _encode(partialMessages), 'log': logMessages, }; } - -List> encode(List messages) => - encodeList(messages, encodePresenceMessage); diff --git a/test_integration/lib/test/realtime_publish_test.dart b/test_integration/lib/test/realtime/realtime_publish_test.dart similarity index 67% rename from test_integration/lib/test/realtime_publish_test.dart rename to test_integration/lib/test/realtime/realtime_publish_test.dart index 3b86e0a7c..f079a2c68 100644 --- a/test_integration/lib/test/realtime_publish_test.dart +++ b/test_integration/lib/test/realtime/realtime_publish_test.dart @@ -1,8 +1,8 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../factory/reporter.dart'; +import '../../factory/reporter.dart'; +import '../../utils/realtime.dart'; Future> testRealtimePublish({ Reporter reporter, @@ -18,16 +18,10 @@ Future> testRealtimePublish({ ..logLevel = LogLevel.verbose ..logHandler = ({msg, exception}) => logMessages.add([msg, '$exception']), ); - await realtimeMessagesPublishUtil(realtime.channels.get('test')); + await publishMessages(realtime.channels.get('test')); await realtime.close(); return { 'handle': await realtime.handle, 'log': logMessages, }; } - -Future realtimeMessagesPublishUtil(RealtimeChannel channel) async { - for (final data in messagesToPublish) { - await channel.publish(name: data[0] as String, data: data[1]); - } -} diff --git a/test_integration/lib/test/realtime_publish_with_auth_callback_test.dart b/test_integration/lib/test/realtime/realtime_publish_with_auth_callback_test.dart similarity index 82% rename from test_integration/lib/test/realtime_publish_with_auth_callback_test.dart rename to test_integration/lib/test/realtime/realtime_publish_with_auth_callback_test.dart index 9614397e3..411068f67 100644 --- a/test_integration/lib/test/realtime_publish_with_auth_callback_test.dart +++ b/test_integration/lib/test/realtime/realtime_publish_with_auth_callback_test.dart @@ -1,8 +1,8 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../factory/reporter.dart'; -import 'realtime_publish_test.dart'; +import '../../factory/reporter.dart'; +import '../../utils/realtime.dart'; Future> testRealtimePublishWithAuthCallback({ Reporter reporter, @@ -16,7 +16,7 @@ Future> testRealtimePublishWithAuthCallback({ authCallbackInvoked = true; return TokenRequest.fromMap(await getTokenRequest()); })); - await realtimeMessagesPublishUtil(realtime.channels.get('test')); + await publishMessages(realtime.channels.get('test')); await realtime.close(); return { diff --git a/test_integration/lib/test/realtime_subscribe.dart b/test_integration/lib/test/realtime/realtime_subscribe.dart similarity index 89% rename from test_integration/lib/test/realtime_subscribe.dart rename to test_integration/lib/test/realtime/realtime_subscribe.dart index af3042850..d005ec852 100644 --- a/test_integration/lib/test/realtime_subscribe.dart +++ b/test_integration/lib/test/realtime/realtime_subscribe.dart @@ -1,24 +1,9 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; -import 'realtime_publish_test.dart'; - -Future> testRealtimeSubscribe({ - Reporter reporter, - Map payload, -}) async { - final appKey = await provision('sandbox-'); - - return { - 'all': await _getAllMessages(appKey.toString(), 'test-all'), - 'filteredWithName': - await _getAllMessages(appKey.toString(), 'test-name', name: 'name1'), - 'filteredWithNames': await _getAllMessages(appKey.toString(), 'test-name', - names: ['name1', 'name2']), - }; -} +import '../../factory/reporter.dart'; +import '../../utils/encoders.dart'; +import '../../utils/realtime.dart'; Future>> _getAllMessages( String apiKey, @@ -42,7 +27,22 @@ Future>> _getAllMessages( channel.subscribe(name: name, names: names).listen((message) { messages.add(encodeMessage(message)); }); - await realtimeMessagesPublishUtil(channel); + await publishMessages(channel); await subscription.cancel(); return messages; } + +Future> testRealtimeSubscribe({ + Reporter reporter, + Map payload, +}) async { + final appKey = await provision('sandbox-'); + + return { + 'all': await _getAllMessages(appKey.toString(), 'test-all'), + 'filteredWithName': + await _getAllMessages(appKey.toString(), 'test-name', name: 'name1'), + 'filteredWithNames': await _getAllMessages(appKey.toString(), 'test-name', + names: ['name1', 'name2']), + }; +} diff --git a/test_integration/lib/test/rest/rest_capability_test.dart b/test_integration/lib/test/rest/rest_capability_test.dart new file mode 100644 index 000000000..0f245928b --- /dev/null +++ b/test_integration/lib/test/rest/rest_capability_test.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:ably_flutter/ably_flutter.dart'; +import 'package:ably_flutter_example/provisioning.dart'; + +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/encoders.dart'; + +Future> testRestCapabilities({ + Reporter reporter, + Map payload, +}) async { + final capabilitySpec = {}; + final combinations = getAllSubsets(['publish', 'history', 'subscribe']) + .where((spec) => spec.isNotEmpty) + .toList(); + + for (var i = 0; i < combinations.length; i++) { + capabilitySpec['channel-$i'] = combinations[i]; + } + final appKey = await provision('sandbox-', { + 'keys': [ + {'capability': jsonEncode(capabilitySpec)}, + ], + }); + + final rest = Rest( + options: ClientOptions.fromKey(appKey.toString()) + ..environment = 'sandbox' + ..clientId = 'someClientId' + ..logLevel = LogLevel.verbose, + ); + + final matrix = []; + for (final entry in capabilitySpec.entries) { + Map publishException; + Map historyException; + Map presenceException; + Map presenceHistoryException; + + final channel = rest.channels.get(entry.key); + try { + await channel.publish(); + } on AblyException catch (exception) { + publishException = encodeAblyException(exception); + } + try { + await channel.history(); + } on AblyException catch (exception) { + historyException = encodeAblyException(exception); + } + try { + await channel.presence.get(); + } on AblyException catch (exception) { + presenceException = encodeAblyException(exception); + } + try { + await channel.presence.history(); + } on AblyException catch (exception) { + presenceHistoryException = encodeAblyException(exception); + } + matrix.add({ + 'channelCapabilities': entry.value, + 'publishException': publishException, + 'historyException': historyException, + 'presenceException': presenceException, + 'presenceHistoryException': presenceHistoryException, + }); + } + + return {'matrix': matrix}; +} diff --git a/test_integration/lib/test/rest_history_test.dart b/test_integration/lib/test/rest/rest_history_test.dart similarity index 55% rename from test_integration/lib/test/rest_history_test.dart rename to test_integration/lib/test/rest/rest_history_test.dart index 7fe1811d8..34f270bcc 100644 --- a/test_integration/lib/test/rest_history_test.dart +++ b/test_integration/lib/test/rest/rest_history_test.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; -import 'rest_publish_test.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/encoders.dart'; +import '../../utils/rest.dart'; Future> testRestHistory({ Reporter reporter, @@ -22,41 +23,41 @@ Future> testRestHistory({ ({msg, exception}) => logMessages.add([msg, exception.toString()]), ); final channel = rest.channels.get('test'); - await restMessagesPublishUtil(channel); + await publishMessages(channel); final paginatedResult = await channel.history(); - final historyDefault = await _history(channel); - await Future.delayed(const Duration(seconds: 2)); + final historyDefault = await getHistory(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit4 = await _history(channel, RestHistoryParams(limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit4 = await getHistory(channel, RestHistoryParams(limit: 4)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit2 = await _history(channel, RestHistoryParams(limit: 2)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit2 = await getHistory(channel, RestHistoryParams(limit: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyForwardLimit4 = await _history( + final historyForwardLimit4 = await getHistory( channel, RestHistoryParams(direction: 'forwards', limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time1 = DateTime.now(); //TODO(tiholic) iOS fails without this delay // - timestamp on message retrieved from history // is earlier than expected when ran in CI - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await channel.publish(name: 'history', data: 'test'); //TODO(tiholic) understand why tests fail without this delay - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time2 = DateTime.now(); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await channel.publish(name: 'history', data: 'test2'); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final historyWithStart = - await _history(channel, RestHistoryParams(start: time1)); + await getHistory(channel, RestHistoryParams(start: time1)); final historyWithStartAndEnd = - await _history(channel, RestHistoryParams(start: time1, end: time2)); - final historyAll = await _history(channel); + await getHistory(channel, RestHistoryParams(start: time1, end: time2)); + final historyAll = await getHistory(channel); return { 'handle': await rest.handle, 'paginatedResult': encodePaginatedResult(paginatedResult, encodeMessage), @@ -72,16 +73,3 @@ Future> testRestHistory({ 'log': logMessages, }; } - -Future>> _history( - RestChannel channel, [ - RestHistoryParams params, -]) async { - var results = await channel.history(params); - final messages = encodeList(results.items, encodeMessage); - while (results.hasNext()) { - results = await results.next(); - messages.addAll(encodeList(results.items, encodeMessage)); - } - return messages; -} diff --git a/test_integration/lib/test/rest_presence_get_test.dart b/test_integration/lib/test/rest/rest_presence_get_test.dart similarity index 58% rename from test_integration/lib/test/rest_presence_get_test.dart rename to test_integration/lib/test/rest/rest_presence_get_test.dart index 97a645871..6485b7a21 100644 --- a/test_integration/lib/test/rest_presence_get_test.dart +++ b/test_integration/lib/test/rest/rest_presence_get_test.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/rest.dart'; final logMessages = >[]; @@ -27,7 +28,7 @@ Future> testRestPresenceGet({ final rest = Rest(options: getClientOptions(appKey)); final channel = rest.channels.get('test'); - final membersInitial = await _members(channel); + final membersInitial = await getPresenceMembers(channel); // enter multiple clients for (var i = 0; i < messagesToPublish.length; i++) { @@ -36,31 +37,37 @@ Future> testRestPresenceGet({ ).channels.get('test').presence.enter(messagesToPublish[i][1]); } - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersDefault = await _members(channel); - await Future.delayed(const Duration(seconds: 2)); + final membersDefault = await getPresenceMembers(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersLimit4 = await _members(channel, RestPresenceParams(limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final membersLimit4 = await getPresenceMembers( + channel, + RestPresenceParams(limit: 4), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersLimit2 = await _members(channel, RestPresenceParams(limit: 2)); - await Future.delayed(const Duration(seconds: 2)); + final membersLimit2 = await getPresenceMembers( + channel, + RestPresenceParams(limit: 2), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final membersClientId = await _members( + final membersClientId = await getPresenceMembers( channel, RestPresenceParams(clientId: 'client-1'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); // TODO(tiholic) extract connection ID from realtime instance // after implementing `id` update on connection object from platform // Until then, `membersConnectionId` will be empty list - final membersConnectionId = await _members( + final membersConnectionId = await getPresenceMembers( channel, RestPresenceParams(connectionId: 'connection-1'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); return { 'handle': await rest.handle, @@ -73,18 +80,3 @@ Future> testRestPresenceGet({ 'log': logMessages, }; } - -Future>> _members( - RestChannel channel, [ - RestPresenceParams params, -]) async { - var results = await channel.presence.get(params); - final messages = - encodeList(results.items, encodePresenceMessage); - while (results.hasNext()) { - results = await results.next(); - messages.addAll( - encodeList(results.items, encodePresenceMessage)); - } - return messages; -} diff --git a/test_integration/lib/test/rest_presence_history_test.dart b/test_integration/lib/test/rest/rest_presence_history_test.dart similarity index 57% rename from test_integration/lib/test/rest_presence_history_test.dart rename to test_integration/lib/test/rest/rest_presence_history_test.dart index 0a13f7cd9..5096c8926 100644 --- a/test_integration/lib/test/rest_presence_history_test.dart +++ b/test_integration/lib/test/rest/rest_presence_history_test.dart @@ -1,9 +1,10 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../config/data.dart'; -import '../config/encoders.dart'; -import '../factory/reporter.dart'; +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/rest.dart'; Future> testRestPresenceHistory({ Reporter reporter, @@ -23,7 +24,7 @@ Future> testRestPresenceHistory({ final rest = Rest(options: options); final channel = rest.channels.get('test'); - final historyInitial = await _history(channel); + final historyInitial = await getPresenceHistory(channel); // creating presence history on channel final realtimePresence = @@ -37,40 +38,50 @@ Future> testRestPresenceHistory({ // leaves channel await realtimePresence.leave(messagesToPublish.last[1]); - final historyDefault = await _history(channel); - await Future.delayed(const Duration(seconds: 2)); + final historyDefault = await getPresenceHistory(channel); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit4 = await _history(channel, RestHistoryParams(limit: 4)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit4 = await getPresenceHistory( + channel, + RestHistoryParams(limit: 4), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyLimit2 = await _history(channel, RestHistoryParams(limit: 2)); - await Future.delayed(const Duration(seconds: 2)); + final historyLimit2 = await getPresenceHistory( + channel, + RestHistoryParams(limit: 2), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyForwards = await _history( + final historyForwards = await getPresenceHistory( channel, RestHistoryParams(direction: 'forwards'), ); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time1 = DateTime.now(); //TODO(tiholic) iOS fails without this delay // - timestamp on message retrieved from history // is earlier than expected when ran in CI - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await realtimePresence.enter('enter-start-time'); // TODO(tiholic) understand why tests fail without this delay - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); final time2 = DateTime.now(); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); await realtimePresence.leave('leave-end-time'); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(TestConstants.publishToHistoryDelay); - final historyWithStart = - await _history(channel, RestHistoryParams(start: time1)); - final historyWithStartAndEnd = - await _history(channel, RestHistoryParams(start: time1, end: time2)); - final historyAll = await _history(channel); + final historyWithStart = await getPresenceHistory( + channel, + RestHistoryParams(start: time1), + ); + final historyWithStartAndEnd = await getPresenceHistory( + channel, + RestHistoryParams(start: time1, end: time2), + ); + final historyAll = await getPresenceHistory(channel); return { 'handle': await rest.handle, @@ -85,18 +96,3 @@ Future> testRestPresenceHistory({ 'log': logMessages, }; } - -Future>> _history( - RestChannel channel, [ - RestHistoryParams params, -]) async { - var results = await channel.presence.history(params); - final messages = - encodeList(results.items, encodePresenceMessage); - while (results.hasNext()) { - results = await results.next(); - messages.addAll( - encodeList(results.items, encodePresenceMessage)); - } - return messages; -} diff --git a/test_integration/lib/test/rest/rest_publish_test.dart b/test_integration/lib/test/rest/rest_publish_test.dart new file mode 100644 index 000000000..6495d8fda --- /dev/null +++ b/test_integration/lib/test/rest/rest_publish_test.dart @@ -0,0 +1,125 @@ +import 'package:ably_flutter/ably_flutter.dart'; +import 'package:ably_flutter_example/provisioning.dart'; + +import '../../config/test_config.dart'; +import '../../factory/reporter.dart'; +import '../../utils/data.dart'; +import '../../utils/encoders.dart'; +import '../../utils/rest.dart'; + +Future> testRestPublish({ + Reporter reporter, + Map payload, +}) async { + reporter.reportLog('init start'); + final appKey = await provision('sandbox-'); + final logMessages = >[]; + + final rest = Rest( + options: ClientOptions.fromKey(appKey.toString()) + ..environment = 'sandbox' + ..clientId = 'someClientId' + ..logLevel = LogLevel.verbose + ..logHandler = + ({msg, exception}) => logMessages.add([msg, exception.toString()]), + ); + await publishMessages(rest.channels.get('test')); + return { + 'handle': await rest.handle, + 'log': logMessages, + }; +} + +Future> testRestPublishSpec({ + Reporter reporter, + Map payload, +}) async { + final appKey = await provision('sandbox-'); + + final rest = Rest( + options: ClientOptions.fromKey(appKey.toString()) + ..environment = 'sandbox' + ..clientId = 'someClientId' + ..logLevel = LogLevel.verbose, + ); + final channel = rest.channels.get('test'); + await channel.publish(); + await channel.publish(name: 'name1'); + await channel.publish(data: 'data1'); + await channel.publish(name: 'name1', data: 'data1'); + await Future.delayed(TestConstants.publishToHistoryDelay); + await channel.publish( + message: Message(name: 'message-name1', data: 'message-data1'), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); + await channel.publish(messages: [ + Message(name: 'messages-name1', data: 'messages-data1'), + Message(name: 'messages-name2', data: 'messages-data2'), + ]); + await Future.delayed(TestConstants.publishToHistoryDelay); + await channel.publish( + message: Message( + name: 'message-name1', data: 'message-data1', clientId: 'someClientId'), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); + + // publishing message with a different client id + Map exception; + try { + await channel.publish( + message: Message(name: 'name', clientId: 'client-id')); + } on AblyException catch (e) { + exception = encodeAblyException(e); + } + final history = await getHistory( + channel, + RestHistoryParams(direction: 'forwards'), + ); + + // client options - no client id, message has client id + final rest2 = Rest( + options: ClientOptions.fromKey(appKey.toString())..environment = 'sandbox', + ); + + final channel2 = rest2.channels.get('test2'); + await channel2.publish( + message: Message(name: 'name-client-id', clientId: 'client-id'), + ); + await Future.delayed(TestConstants.publishToHistoryDelay); + final history2 = await getHistory( + channel2, + RestHistoryParams(direction: 'forwards'), + ); + + // publish max allowed length - sandbox apps message limit is 16384 + Map exception2; + try { + await channel2.publish(data: getRandomString(16384)); + } on AblyException catch (e) { + exception2 = encodeAblyException(e); + } + + // publish more than max allowed length + Map exception3; + try { + await channel2.publish(data: getRandomString(16384 + 1)); + } on AblyException catch (e) { + exception3 = encodeAblyException(e); + } + + final channel3 = rest2.channels.get('©Äblý'); + await channel3.publish(name: 'Ωπ', data: 'ΨΔ'); + + await Future.delayed(TestConstants.publishToHistoryDelay); + final history3 = await getHistory(channel3); + + return { + 'handle': await rest.handle, + 'publishedMessages': history, + 'publishedMessages2': history2, + 'publishedMessages3': history3, + 'exception': exception, + 'exception2': exception2, + 'exception3': exception3, + }; +} diff --git a/test_integration/lib/test/rest_publish_with_auth_callback_test.dart b/test_integration/lib/test/rest/rest_publish_with_auth_callback_test.dart similarity index 84% rename from test_integration/lib/test/rest_publish_with_auth_callback_test.dart rename to test_integration/lib/test/rest/rest_publish_with_auth_callback_test.dart index 30dd1335d..1be906e80 100644 --- a/test_integration/lib/test/rest_publish_with_auth_callback_test.dart +++ b/test_integration/lib/test/rest/rest_publish_with_auth_callback_test.dart @@ -1,8 +1,8 @@ import 'package:ably_flutter/ably_flutter.dart'; import 'package:ably_flutter_example/provisioning.dart'; -import '../factory/reporter.dart'; -import 'rest_publish_test.dart'; +import '../../factory/reporter.dart'; +import '../../utils/rest.dart'; Future> testRestPublishWithAuthCallback({ Reporter reporter, @@ -18,7 +18,7 @@ Future> testRestPublishWithAuthCallback({ authCallbackInvoked = true; return TokenRequest.fromMap(await getTokenRequest()); })); - await restMessagesPublishUtil(rest.channels.get('test')); + await publishMessages(rest.channels.get('test')); return { 'handle': await rest.handle, 'authCallbackInvoked': authCallbackInvoked diff --git a/test_integration/lib/test/rest_publish_test.dart b/test_integration/lib/test/rest_publish_test.dart deleted file mode 100644 index f2b2f90e7..000000000 --- a/test_integration/lib/test/rest_publish_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:ably_flutter/ably_flutter.dart'; -import 'package:ably_flutter_example/provisioning.dart'; - -import '../config/data.dart'; -import '../factory/reporter.dart'; - -Future> testRestPublish({ - Reporter reporter, - Map payload, -}) async { - reporter.reportLog('init start'); - final appKey = await provision('sandbox-'); - final logMessages = >[]; - - final rest = Rest( - options: ClientOptions.fromKey(appKey.toString()) - ..environment = 'sandbox' - ..clientId = 'someClientId' - ..logLevel = LogLevel.verbose - ..logHandler = - ({msg, exception}) => logMessages.add([msg, exception.toString()]), - ); - await restMessagesPublishUtil(rest.channels.get('test')); - return { - 'handle': await rest.handle, - 'log': logMessages, - }; -} - -Future restMessagesPublishUtil(RestChannel channel) async { - for (final data in messagesToPublish) { - await channel.publish(name: data[0] as String, data: data[1]); - } -} diff --git a/test_integration/lib/utils/data.dart b/test_integration/lib/utils/data.dart new file mode 100644 index 000000000..8175e28b8 --- /dev/null +++ b/test_integration/lib/utils/data.dart @@ -0,0 +1,54 @@ +import 'dart:math'; + +final messagesToPublish = [ + [null, null], //name and message are both null + [null, 'Ably'], //name is null + ['name1', null], //message is null + ['name1', 'Ably'], //message is a string + [ + 'name2', + [1, 2, 3] + ], //message is a numeric list + [ + 'name2', + ['hello', 'ably'] + ], //message is a string list + [ + 'name3', + { + 'hello': 'ably', + 'items': ['1', 2.2, true] + } + ], //message is a map + [ + 'name3', + [ + {'hello': 'ably'}, + 'ably', + 'realtime' + ] + ] //message is a complex list +]; + +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; +Random _rnd = Random(); + +/// get random alpha numeric string of given length +String getRandomString(int length) => String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)), + ), + ); + +/// returns subsets of all entries in a list +List getAllSubsets(List l) => l.fold>( + [[]], + (subLists, element) => subLists + .map((subList) => [ + subList, + subList + [element] + ]) + .expand((element) => element) + .toList(), + ); diff --git a/test_integration/lib/config/encoders.dart b/test_integration/lib/utils/encoders.dart similarity index 80% rename from test_integration/lib/config/encoders.dart rename to test_integration/lib/utils/encoders.dart index d5fca0068..e45796450 100644 --- a/test_integration/lib/config/encoders.dart +++ b/test_integration/lib/utils/encoders.dart @@ -56,3 +56,16 @@ Map encodePaginatedResult( 'hasNext': paginatedResult.hasNext(), 'isLast': paginatedResult.isLast(), }; + +Map encodeAblyException(AblyException exception) => { + 'code': exception.code, + 'message': exception.message, + 'errorInfo': { + 'code': exception.errorInfo.code, + 'href': exception.errorInfo.href, + 'message': exception.errorInfo.message, + 'cause': exception.errorInfo.cause, + 'statusCode': exception.errorInfo.statusCode, + 'requestId': exception.errorInfo.requestId, + } + }; diff --git a/test_integration/lib/utils/realtime.dart b/test_integration/lib/utils/realtime.dart new file mode 100644 index 000000000..415037b41 --- /dev/null +++ b/test_integration/lib/utils/realtime.dart @@ -0,0 +1,48 @@ +import 'package:ably_flutter/ably_flutter.dart'; + +import 'data.dart'; +import 'encoders.dart'; + +Future publishMessages(RealtimeChannel channel) async { + for (final data in messagesToPublish) { + await channel.publish(name: data[0] as String, data: data[1]); + } +} + +Future>> getHistory( + RealtimeChannel channel, [ + RealtimeHistoryParams params, +]) async { + var results = await channel.history(params); + final messages = encodeList(results.items, encodeMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll(encodeList(results.items, encodeMessage)); + } + return messages; +} + +Future>> getPresenceMembers( + RealtimeChannel channel, [ + RealtimePresenceParams params, +]) async => + encodeList( + await channel.presence.get(params), + encodePresenceMessage, + ); + +Future>> getPresenceHistory( + RealtimeChannel channel, [ + RealtimeHistoryParams params, +]) async { + var results = await channel.presence.history(params); + final messages = + encodeList(results.items, encodePresenceMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll( + encodeList(results.items, encodePresenceMessage), + ); + } + return messages; +} diff --git a/test_integration/lib/utils/rest.dart b/test_integration/lib/utils/rest.dart new file mode 100644 index 000000000..1d2459922 --- /dev/null +++ b/test_integration/lib/utils/rest.dart @@ -0,0 +1,53 @@ +import 'package:ably_flutter/ably_flutter.dart'; + +import 'data.dart'; +import 'encoders.dart'; + +Future publishMessages(RestChannel channel) async { + for (final data in messagesToPublish) { + await channel.publish(name: data[0] as String, data: data[1]); + } +} + +Future>> getHistory( + RestChannel channel, [ + RestHistoryParams params, +]) async { + var results = await channel.history(params); + final messages = encodeList(results.items, encodeMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll(encodeList(results.items, encodeMessage)); + } + return messages; +} + +Future>> getPresenceHistory( + RestChannel channel, [ + RestHistoryParams params, +]) async { + var results = await channel.presence.history(params); + final messages = + encodeList(results.items, encodePresenceMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll( + encodeList(results.items, encodePresenceMessage)); + } + return messages; +} + +Future>> getPresenceMembers( + RestChannel channel, [ + RestPresenceParams params, +]) async { + var results = await channel.presence.get(params); + final messages = + encodeList(results.items, encodePresenceMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll( + encodeList(results.items, encodePresenceMessage)); + } + return messages; +} diff --git a/test_integration/pubspec.lock b/test_integration/pubspec.lock index 9002beb2c..8c981ecc9 100644 --- a/test_integration/pubspec.lock +++ b/test_integration/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "12.0.0" + version: "21.0.0" ably_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.2.0-preview.1" + version: "1.2.0-preview.2" ably_flutter_example: dependency: "direct main" description: @@ -28,28 +28,28 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.40.6" + version: "1.5.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "3.1.2" args: dependency: "direct main" description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.1.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" boolean_selector: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" clock: dependency: transitive description: @@ -98,21 +98,21 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.0" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.14.2" + version: "1.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.1" fake_async: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "6.1.0" flutter: dependency: "direct main" description: flutter @@ -153,35 +153,35 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.1" http: - dependency: "direct dev" + dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.3" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.0" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "1.0.0" js: dependency: transitive description: @@ -195,7 +195,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.1" matcher: dependency: transitive description: @@ -216,35 +216,21 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.7" - node_interop: - dependency: transitive - description: - name: node_interop - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - node_io: - dependency: transitive - description: - name: node_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" + version: "1.0.0" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.13" + version: "2.0.0" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.0.0" path: dependency: transitive description: @@ -279,49 +265,49 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.2.1" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.0.0" retry: dependency: transitive description: name: retry url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.0" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.1.4" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.9+2" + version: "1.0.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -347,7 +333,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -375,7 +361,7 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" term_glyph: dependency: transitive description: @@ -389,21 +375,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.5" + version: "1.16.7" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.3.0" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.15" + version: "0.3.18" typed_data: dependency: transitive description: @@ -424,42 +410,42 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" + version: "6.2.0" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "2.1.0" webdriver: dependency: transitive description: name: webdriver url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "3.0.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "1.0.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.0" sdks: - dart: ">=2.12.0-0.0 <3.0.0" + dart: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" diff --git a/test_integration/pubspec.yaml b/test_integration/pubspec.yaml index 5802505dc..669ec00e8 100644 --- a/test_integration/pubspec.yaml +++ b/test_integration/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: path: '..' ably_flutter_example: path: '../example' - args: ^1.6.0 + args: ^2.0.0 flutter: sdk: flutter @@ -20,8 +20,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - http: ^0.12.2 - test: ^1.9.4 + test: 1.16.7 flutter: uses-material-design: true diff --git a/test_integration/test_driver/test_implementation/basic_platform_tests.dart b/test_integration/test_driver/test_implementation/basic_platform_tests.dart index 6ca68bf1d..304b8db12 100644 --- a/test_integration/test_driver/test_implementation/basic_platform_tests.dart +++ b/test_integration/test_driver/test_implementation/basic_platform_tests.dart @@ -2,49 +2,66 @@ import 'package:ably_flutter_integration_test/driver_data_handler.dart'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; -Future testPlatformAndAblyVersion(FlutterDriver driver) async { - final data = {'message': 'foo'}; - final message = - TestControlMessage(TestName.platformAndAblyVersion, payload: data); - - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['platformVersion'], isA()); - expect(response.payload['platformVersion'], isNot(isEmpty)); - expect(response.payload['ablyVersion'], isA()); - expect(response.payload['ablyVersion'], isNot(isEmpty)); +void testPlatformAndAblyVersion(FlutterDriver Function() getDriver) { + const message = TestControlMessage(TestName.platformAndAblyVersion); + TestControlMessage response; + setUpAll(() async => response = await getTestResponse(getDriver(), message)); + + test('platformVersion is a string', () { + expect(response.payload['platformVersion'], isA()); + }); + + test('platformVersion is not empty', () { + expect(response.payload['platformVersion'], isNot(isEmpty)); + }); + + test('ablyVersion is a string', () { + expect(response.payload['ablyVersion'], isA()); + }); + + test('ablyVersion is not empty', () { + expect(response.payload['ablyVersion'], isNot(isEmpty)); + }); } -Future testDemoDependencies(FlutterDriver driver) async { - final data = {'message': 'foo'}; - final message = - TestControlMessage(TestName.appKeyProvisioning, payload: data); - - final response = await getTestResponse(driver, message); - - print('response.payload ${response.payload}'); - - expect(response.testName, message.testName); - - expect(response.payload['appKey'], isA()); - expect(response.payload['appKey'], isNotEmpty); - - print('response.payload:: ${response.payload}'); - - final tokenRequest = response.payload['tokenRequest']; - - expect(tokenRequest['keyName'], isA()); - expect(tokenRequest['keyName'], isNotEmpty); - - expect(tokenRequest['nonce'], isA()); - expect(tokenRequest['nonce'], isNotEmpty); - - expect(tokenRequest['mac'], isA()); - expect(tokenRequest['mac'], isNotEmpty); - - expect(tokenRequest['timestamp'], isA()); - - expect(tokenRequest['ttl'], isA()); +void testDemoDependencies(FlutterDriver Function() getDriver) { + const message = TestControlMessage(TestName.appKeyProvisioning); + TestControlMessage response; + setUpAll(() async => response = await getTestResponse(getDriver(), message)); + + test('appKey is a string', () { + expect(response.payload['appKey'], isA()); + expect(response.payload['appKey'], isNotEmpty); + }); + + group('token request has', () { + Map tokenRequest; + + setUp(() { + tokenRequest = response.payload['tokenRequest'] as Map; + }); + + test('non-empty keyName', () { + expect(tokenRequest['keyName'], isA()); + expect(tokenRequest['keyName'], isNotEmpty); + }); + + test('non-empty nonce', () { + expect(tokenRequest['nonce'], isA()); + expect(tokenRequest['nonce'], isNotEmpty); + }); + + test('non-empty mac', () { + expect(tokenRequest['mac'], isA()); + expect(tokenRequest['mac'], isNotEmpty); + }); + + test('non-empty timestamp', () { + expect(tokenRequest['timestamp'], isA()); + }); + + test('non-empty ttl', () { + expect(tokenRequest['ttl'], isA()); + }); + }); } diff --git a/test_integration/test_driver/test_implementation/helper_tests.dart b/test_integration/test_driver/test_implementation/helper_tests.dart index 1ccd9e157..70fc4cf6e 100644 --- a/test_integration/test_driver/test_implementation/helper_tests.dart +++ b/test_integration/test_driver/test_implementation/helper_tests.dart @@ -2,13 +2,13 @@ import 'package:ably_flutter_integration_test/driver_data_handler.dart'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; -Future testShouldReportUnhandledException(FlutterDriver driver) async { +void testShouldReportUnhandledException(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.testHelperUnhandledExceptionTest); + TestControlMessage response; + setUpAll(() async => response = await getTestResponse(getDriver(), message)); - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['error']['exceptionType'], 'String'); - expect(response.payload['error']['exception'], contains('Unhandled')); + test('returns appropriate error message', () { + expect(response.payload['error']['exceptionType'], 'String'); + expect(response.payload['error']['exception'], contains('Unhandled')); + }); } diff --git a/test_integration/test_driver/test_implementation/realtime_tests.dart b/test_integration/test_driver/test_implementation/realtime_tests.dart index 53eff0be9..70d53a725 100644 --- a/test_integration/test_driver/test_implementation/realtime_tests.dart +++ b/test_integration/test_driver/test_implementation/realtime_tests.dart @@ -4,393 +4,456 @@ import 'package:test/test.dart'; import 'utils.dart'; -Future testRealtimePublish(FlutterDriver driver) async { +void testRealtimePublish(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimePublish); - - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); + const message2 = TestControlMessage(TestName.realtimePublishWithAuthCallback); + TestControlMessage response; + TestControlMessage response2; + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + response2 = await getTestResponse(getDriver(), message2); + }); + + test('publishes message without any response', () { + expect(response.payload['handle'], isA()); + expect(response.payload['handle'], greaterThan(0)); + }); + test('invokes authCallback if available in clientOptions', () { + expect(response2.payload['authCallbackInvoked'], isTrue); + }); } -Future testRealtimeEvents(FlutterDriver driver) async { +void testRealtimeEvents(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimeEvents); + TestControlMessage response; + List connectionStates; + List> connectionStateChanges; + List> filteredConnectionStateChanges; + List channelStates; + List> channelStateChanges; + List> filteredChannelStateChanges; + + List transformState(items) => + List.from(items as List).map((t) => t as String).toList(); + + List> transformStateChange(items) => + List.from(items as List).map((t) => t as Map).toList(); - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - final connectionStates = (response.payload['connectionStates'] as List) - .map((e) => e as String) - .toList(); - final connectionStateChanges = - (response.payload['connectionStateChanges'] as List) - .map((e) => e as Map) - .toList(); - final filteredConnectionStateChanges = - (response.payload['filteredConnectionStateChanges'] as List) - .map((e) => e as Map) - .toList(); - final channelStates = (response.payload['channelStates'] as List) - .map((e) => e as String) - .toList(); - final channelStateChanges = (response.payload['channelStateChanges'] as List) - .map((e) => e as Map) - .toList(); - final filteredChannelStateChanges = - (response.payload['filteredChannelStateChanges'] as List) - .map((e) => e as Map) - .toList(); - - // connectionStates - expect( - connectionStates, - orderedEquals(const [ - 'initialized', - 'initialized', - 'connected', - 'connected', - 'closed', - ])); - - // connectionStateChanges - expect( - connectionStateChanges.map((e) => e['event']), - orderedEquals(const [ - 'connecting', - 'connected', - 'closing', - 'closed', - ])); - - expect( - connectionStateChanges.map((e) => e['current']), - orderedEquals(const [ - 'connecting', - 'connected', - 'closing', - 'closed', - ])); - - expect( - connectionStateChanges.map((e) => e['previous']), - orderedEquals(const [ - 'initialized', - 'connecting', - 'connected', - 'closing', - ])); - - // filteredConnectionStateChanges - expect(filteredConnectionStateChanges.map((e) => e['event']), const [ - 'connected', - ]); - - expect(filteredConnectionStateChanges.map((e) => e['current']), const [ - 'connected', - ]); - - expect(filteredConnectionStateChanges.map((e) => e['previous']), const [ - 'connecting', - ]); - - // channelStates - expect( - channelStates, - orderedEquals(const [ - 'initialized', - 'initialized', - 'attached', - 'attached', - 'detached', - 'detached', - ])); - - // channelStateChanges - - // TODO(tiholic): get rid of _stateChangeEvents and _stateChangePrevious - // variables as they are a way to make tests pass due to - // https://github.com/ably/ably-flutter/issues/63 - List _stateChangeEvents; - List _stateChangePrevious; - if (channelStateChanges.length == 5) { - // ios - _stateChangeEvents = const [ - 'attaching', - 'attached', - 'detaching', - 'detached', - 'detached', - ]; - _stateChangePrevious = const [ - 'initialized', - 'attaching', - 'attached', - 'detaching', - 'detached', - ]; - } else { - _stateChangeEvents = const [ - 'attaching', - 'attached', - 'detaching', - 'detached', - ]; - _stateChangePrevious = const [ - 'initialized', - 'attaching', - 'attached', - 'detaching', - ]; - } - - expect(channelStateChanges.map((e) => e['event']), - orderedEquals(_stateChangeEvents)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + connectionStates = transformState(response.payload['connectionStates']); + connectionStateChanges = transformStateChange( + response.payload['connectionStateChanges'], + ); + filteredConnectionStateChanges = transformStateChange( + response.payload['filteredConnectionStateChanges'], + ); + channelStates = transformState(response.payload['channelStates']); + channelStateChanges = transformStateChange( + response.payload['channelStateChanges'], + ); + filteredChannelStateChanges = transformStateChange( + response.payload['filteredChannelStateChanges'], + ); + }); - expect(channelStateChanges.map((e) => e['current']), - orderedEquals(_stateChangeEvents)); + group('realtime#channel#connection', () { + test('#state', () { + expect( + connectionStates, + orderedEquals(const [ + 'initialized', + 'initialized', + 'connected', + 'connected', + 'closed', + ])); + }); + test( + '#on returns a stream which can be subscribed for connectionStateChanges', + () { + expect( + connectionStateChanges.map((e) => e['event']), + orderedEquals(const [ + 'connecting', + 'connected', + 'closing', + 'closed', + ])); + expect( + connectionStateChanges.map((e) => e['previous']), + orderedEquals(const [ + 'initialized', + 'connecting', + 'connected', + 'closing', + ])); + }, + ); - expect(channelStateChanges.map((e) => e['previous']), - orderedEquals(_stateChangePrevious)); + test( + '#on returns a stream which can be subscribed' + ' for connection state changes with filter', + () { + expect(filteredConnectionStateChanges.map((e) => e['event']), const [ + 'connected', + ]); + expect(filteredConnectionStateChanges.map((e) => e['current']), const [ + 'connected', + ]); + expect(filteredConnectionStateChanges.map((e) => e['previous']), const [ + 'connecting', + ]); + }, + ); + }); + + group('realtime#channel#chanenls#channel', () { + test( + '#state', + () { + expect( + channelStates, + orderedEquals(const [ + 'initialized', + 'initialized', + 'attached', + 'attached', + 'detached', + 'detached', + ])); + }, + ); + test( + '#on returns a stream which can be subscribed for channel state changes', + () { + // TODO(tiholic): get rid of _stateChangeEvents and _stateChangePrevious + // variables as they are a way to make tests pass due to + // https://github.com/ably/ably-flutter/issues/63 + List _stateChangeEvents; + List _stateChangePrevious; + if (channelStateChanges.length == 5) { + // ios + _stateChangeEvents = const [ + 'attaching', + 'attached', + 'detaching', + 'detached', + 'detached', + ]; + _stateChangePrevious = const [ + 'initialized', + 'attaching', + 'attached', + 'detaching', + 'detached', + ]; + } else { + _stateChangeEvents = const [ + 'attaching', + 'attached', + 'detaching', + 'detached', + ]; + _stateChangePrevious = const [ + 'initialized', + 'attaching', + 'attached', + 'detaching', + ]; + } + + expect(channelStateChanges.map((e) => e['event']), + orderedEquals(_stateChangeEvents)); + + expect(channelStateChanges.map((e) => e['current']), + orderedEquals(_stateChangeEvents)); + + expect(channelStateChanges.map((e) => e['previous']), + orderedEquals(_stateChangePrevious)); + }, + ); - // filteredChannelStateChanges - expect(filteredChannelStateChanges.map((e) => e['event']), - orderedEquals(const ['attaching'])); + test( + '#on returns a stream which can be subscribed' + ' for channel state changes with filter', + () { + // filteredChannelStateChanges + expect(filteredChannelStateChanges.map((e) => e['event']), + orderedEquals(const ['attaching'])); - expect(filteredChannelStateChanges.map((e) => e['current']), - orderedEquals(const ['attaching'])); + expect(filteredChannelStateChanges.map((e) => e['current']), + orderedEquals(const ['attaching'])); - expect(filteredChannelStateChanges.map((e) => e['previous']), - orderedEquals(const ['initialized'])); + expect(filteredChannelStateChanges.map((e) => e['previous']), + orderedEquals(const ['initialized'])); + }, + ); + }); } -Future testRealtimeSubscribe(FlutterDriver driver) async { +void testRealtimeSubscribe(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimeSubscribe); + TestControlMessage response; + List> all; + List> filteredWithName; + List> filteredWithNames; + + List> transformMessages(messages) => + List.from(messages as List) + .map((t) => t as Map) + .toList(); - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - // Testing realtime subscribe to all messages - final all = response.payload['all'] - .map>( - (m) => Map.castFrom(m as Map)) - .toList(); - - testAllPublishedMessages(all); - - // Testing realtime subscribe to messages filtered with name - final filteredWithName = response.payload['filteredWithName'] - .map>( - (m) => Map.castFrom(m as Map)) - .toList(); - - expect(filteredWithName, isA>>()); - expect(filteredWithName.length, equals(2)); - - expect(filteredWithName[0]['name'], 'name1'); - expect(filteredWithName[0]['data'], isNull); - - expect(filteredWithName[1]['name'], 'name1'); - expect(filteredWithName[1]['data'], equals('Ably')); - - // Testing realtime subscribe to messages filtered with multiple names - final filteredWithNames = response.payload['filteredWithNames'] - .map>( - (m) => Map.castFrom(m as Map)) - .toList(); - - expect(filteredWithNames, isA>>()); - expect(filteredWithNames.length, equals(4)); - - expect(filteredWithNames[0]['name'], 'name1'); - expect(filteredWithNames[0]['data'], isNull); - - expect(filteredWithNames[1]['name'], 'name1'); - expect(filteredWithNames[1]['data'], equals('Ably')); - - expect(filteredWithNames[2]['name'], 'name2'); - expect(filteredWithNames[2]['data'], equals([1, 2, 3])); - - expect(filteredWithNames[3]['name'], 'name2'); - expect(filteredWithNames[3]['data'], equals(['hello', 'ably'])); -} - -Future testRealtimePublishWithAuthCallback(FlutterDriver driver) async { - const message = TestControlMessage(TestName.realtimePublishWithAuthCallback); - - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); - - expect(response.payload['authCallbackInvoked'], isTrue); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + all = transformMessages(response.payload['all']); + filteredWithName = transformMessages(response.payload['filteredWithName']); + filteredWithNames = transformMessages( + response.payload['filteredWithNames'], + ); + }); + + test( + 'realtime#channels#channel#subscribe should subscribe to' + ' all message on channel', + () { + testAllPublishedMessages(all); + }, + ); + + test( + 'realtime#channels#channel#subscribe(name: string)' + ' should subscribe to messages with specified name', + () { + expect(filteredWithName.length, equals(2)); + + expect(filteredWithName[0]['name'], 'name1'); + expect(filteredWithName[0]['data'], isNull); + + expect(filteredWithName[1]['name'], 'name1'); + expect(filteredWithName[1]['data'], equals('Ably')); + }, + ); + + test( + 'realtime#channels#channel#subscribe(names: List)' + ' should subscribe to messages with specified names', + () { + expect(filteredWithNames.length, equals(4)); + + expect(filteredWithNames[0]['name'], 'name1'); + expect(filteredWithNames[0]['data'], isNull); + + expect(filteredWithNames[1]['name'], 'name1'); + expect(filteredWithNames[1]['data'], equals('Ably')); + + expect(filteredWithNames[2]['name'], 'name2'); + expect(filteredWithNames[2]['data'], equals([1, 2, 3])); + + expect(filteredWithNames[3]['name'], 'name2'); + expect(filteredWithNames[3]['data'], equals(['hello', 'ably'])); + }, + ); } -Future testRealtimeHistory(FlutterDriver driver) async { +void testRealtimeHistory(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimeHistory); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); - - final paginatedResult = - response.payload['paginatedResult'] as Map; + Map paginatedResult; + List> historyDefault; + List> historyLimit4; + List> historyLimit2; + List> historyForwardLimit4; + List> historyWithStart; + List> historyWithStartAndEnd; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - final historyDefault = transform(response.payload['historyDefault']); - final historyLimit4 = transform(response.payload['historyLimit4']); - final historyLimit2 = transform(response.payload['historyLimit2']); - final historyForwardLimit4 = - transform(response.payload['historyForwardLimit4']); - final historyWithStart = transform(response.payload['historyWithStart']); - final historyWithStartAndEnd = - transform(response.payload['historyWithStartAndEnd']); - - expect(paginatedResult['hasNext'], false); - expect(paginatedResult['isLast'], true); - expect(paginatedResult['items'], isA()); - - expect(historyDefault.length, equals(8)); - expect(historyLimit4.length, equals(8)); - expect(historyLimit2.length, equals(8)); - expect(historyForwardLimit4.length, equals(8)); - expect(historyWithStart.length, equals(2)); - expect(historyWithStartAndEnd.length, equals(1)); - - testAllPublishedMessages(historyDefault.reversed.toList()); - testAllPublishedMessages(historyLimit4.reversed.toList()); - testAllPublishedMessages(historyLimit2.reversed.toList()); - testAllPublishedMessages(historyForwardLimit4); - - // start and no-end test (backward) - expect(historyWithStart[0]['name'], equals('history')); - expect(historyWithStart[0]['data'], equals('test2')); - - expect(historyWithStart[1]['name'], equals('history')); - expect(historyWithStart[1]['data'], equals('test')); - - // start and end test - expect(historyWithStartAndEnd[0]['name'], equals('history')); - expect(historyWithStartAndEnd[0]['data'], equals('test')); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + paginatedResult = + response.payload['paginatedResult'] as Map; + historyDefault = transform(response.payload['historyDefault']); + historyLimit4 = transform(response.payload['historyLimit4']); + historyLimit2 = transform(response.payload['historyLimit2']); + historyForwardLimit4 = transform(response.payload['historyForwardLimit4']); + historyWithStart = transform(response.payload['historyWithStart']); + historyWithStartAndEnd = transform( + response.payload['historyWithStartAndEnd'], + ); + }); + + group('paginated result', () { + test('#items is a list', () { + expect(paginatedResult['items'], isA()); + }); + test('#hasNext indicates whether there are more entries', () { + expect(paginatedResult['hasNext'], false); + }); + test('#isLast indicates if current page is last page', () { + expect(paginatedResult['isLast'], true); + }); + }); + + group('realtime#channels#channel#history', () { + test('queries all entries by default', () { + expect(historyDefault.length, equals(8)); + testAllPublishedMessages(historyDefault.reversed.toList()); + }); + test('queries all entries by paginating with limit', () { + expect(historyLimit4.length, equals(8)); + expect(historyLimit2.length, equals(8)); + testAllPublishedMessages(historyLimit4.reversed.toList()); + testAllPublishedMessages(historyLimit2.reversed.toList()); + }); + test('queries entries in reverse order with direction set to "forward"', + () { + expect(historyForwardLimit4.length, equals(8)); + testAllPublishedMessages(historyForwardLimit4); + }); + test('returns entries created after specified time', () { + expect(historyWithStart.length, equals(2)); + expect(historyWithStart[0]['name'], equals('history')); + expect(historyWithStart[0]['data'], equals('test2')); + expect(historyWithStart[1]['name'], equals('history')); + expect(historyWithStart[1]['data'], equals('test')); + }); + test('returns entries created between specified start and end', () { + expect(historyWithStartAndEnd.length, equals(1)); + expect(historyWithStartAndEnd[0]['name'], equals('history')); + expect(historyWithStartAndEnd[0]['data'], equals('test')); + }); + }); } -Future testRealtimePresenceGet(FlutterDriver driver) async { +void testRealtimePresenceGet(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimePresenceGet); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); + List> membersInitial; + List> membersDefault; + List> membersClientId; + List> membersConnectionId; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - int timestampSorter(Map a, Map b) { - if (DateTime.parse(a['timestamp'] as String).millisecondsSinceEpoch > - DateTime.parse(b['timestamp'] as String).millisecondsSinceEpoch) { - return 1; - } else { - return -1; - } - } - - final membersInitial = transform(response.payload['membersInitial']); - expect(membersInitial.length, equals(0)); - - final membersDefault = transform(response.payload['membersDefault']); - expect(membersDefault.length, equals(8)); - testAllPresenceMembers(membersDefault..sort(timestampSorter)); - - // there is only 1 client with clientId 'client-1 - final membersClientId = transform(response.payload['membersClientId']); - expect(membersClientId.length, equals(1)); - expect(membersClientId[0]['clientId'], equals('client-1')); - checkMessageData(1, membersClientId[0]['data']); - - // TODO similarly check for membersConnectionId after implementing - // connection id (sync from platform) on realtime connection - final membersConnectionId = - transform(response.payload['membersConnectionId']); - expect(membersConnectionId.length, equals(0)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + membersInitial = transform(response.payload['membersInitial']); + membersDefault = transform(response.payload['membersDefault']); + membersClientId = transform(response.payload['membersClientId']); + membersConnectionId = transform(response.payload['membersConnectionId']); + }); + + group('realtime#channels#channel#presence#get', () { + test('has 0 members without any clients joined', () { + expect(membersInitial.length, equals(0)); + }); + test('queries all entries by default', () { + expect(membersDefault.length, equals(8)); + testAllPresenceMembers(membersDefault..sort(timestampSorter)); + }); + test('filters entries with clientId when specified', () { + // there is only 1 client with clientId 'client-1 + expect(membersClientId.length, equals(1)); + expect(membersClientId[0]['clientId'], equals('client-1')); + checkMessageData(1, membersClientId[0]['data']); + }); + test('filters entries with clientId when specified', () { + // TODO similarly check for membersConnectionId after implementing + // connection id (sync from platform) on realtime connection + expect(membersConnectionId.length, equals(0)); + }); + }); } -Future testRealtimePresenceHistory(FlutterDriver driver) async { +void testRealtimePresenceHistory(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimePresenceHistory); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); + List> historyInitial; + List> historyDefault; + List> historyLimit4; + List> historyLimit2; + List> historyForwards; + List> historyWithStart; + List> historyWithStartAndEnd; + List> historyAll; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - final historyInitial = transform(response.payload['historyInitial']); - expect(historyInitial.length, equals(0)); - - final historyDefault = transform(response.payload['historyDefault']); - expect(historyDefault.length, equals(8)); - testAllPresenceMessagesHistory(historyDefault.reversed.toList()); - - final historyLimit4 = transform(response.payload['historyLimit4']); - expect(historyLimit4.length, equals(8)); - testAllPresenceMessagesHistory(historyLimit4.reversed.toList()); - - final historyLimit2 = transform(response.payload['historyLimit2']); - expect(historyLimit2.length, equals(8)); - testAllPresenceMessagesHistory(historyLimit2.reversed.toList()); - - final historyForwards = transform(response.payload['historyForwards']); - expect(historyForwards.length, equals(8)); - testAllPresenceMessagesHistory(historyForwards.toList()); - - final historyWithStart = - transform(response.payload['historyWithStart']).reversed.toList(); - expect(historyWithStart.length, equals(2)); - expect(historyWithStart[0]['clientId'], equals('someClientId')); - expect(historyWithStart[0]['data'], equals('enter-start-time')); - expect(historyWithStart[1]['clientId'], equals('someClientId')); - expect(historyWithStart[1]['data'], equals('leave-end-time')); - - final historyWithStartAndEnd = - transform(response.payload['historyWithStartAndEnd']); - expect(historyWithStartAndEnd.length, equals(1)); - expect(historyWithStartAndEnd[0]['clientId'], equals('someClientId')); - expect(historyWithStartAndEnd[0]['data'], equals('enter-start-time')); - - final historyAll = transform(response.payload['historyAll']); - expect(historyAll.length, equals(10)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + historyInitial = transform(response.payload['historyInitial']); + historyDefault = transform(response.payload['historyDefault']); + historyLimit4 = transform(response.payload['historyLimit4']); + historyLimit2 = transform(response.payload['historyLimit2']); + historyForwards = transform(response.payload['historyForwards']); + historyWithStart = transform( + response.payload['historyWithStart'], + ).reversed.toList(); + historyWithStartAndEnd = transform( + response.payload['historyWithStartAndEnd'], + ); + historyAll = transform(response.payload['historyAll']); + }); + + test('queries all entries by default', () { + expect(historyInitial.length, equals(0)); + expect(historyAll.length, equals(10)); + + expect(historyDefault.length, equals(8)); + testAllPresenceMessagesHistory(historyDefault.reversed.toList()); + }); + test('queries all entries by paginating with limit', () { + expect(historyLimit4.length, equals(8)); + expect(historyLimit2.length, equals(8)); + testAllPresenceMessagesHistory(historyLimit4.reversed.toList()); + testAllPresenceMessagesHistory(historyLimit2.reversed.toList()); + }); + test( + 'queries entries in reverse order with direction set to "forward"', + () { + expect(historyForwards.length, equals(8)); + testAllPresenceMessagesHistory(historyForwards.toList()); + }, + ); + test('returns entries created after specified time', () { + expect(historyWithStart.length, equals(2)); + expect(historyWithStart[0]['clientId'], equals('someClientId')); + expect(historyWithStart[0]['data'], equals('enter-start-time')); + expect(historyWithStart[1]['clientId'], equals('someClientId')); + expect(historyWithStart[1]['data'], equals('leave-end-time')); + }); + test('returns entries created between specified start and end', () { + expect(historyWithStartAndEnd.length, equals(1)); + expect(historyWithStartAndEnd[0]['clientId'], equals('someClientId')); + expect(historyWithStartAndEnd[0]['data'], equals('enter-start-time')); + }); } -Future testRealtimeEnterUpdateLeave(FlutterDriver driver) async { +void testRealtimeEnterUpdateLeave(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimePresenceEnterUpdateLeave); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['clientIDClashMatrix'], isA()); - expect(response.payload['actionMatrix'], isA()); + List> clientIDClashMatrix; + List> actionMatrix; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + clientIDClashMatrix = transform(response.payload['clientIDClashMatrix']); + actionMatrix = transform(response.payload['actionMatrix']); + }); + void testMatrixEntry( - entry, { + Map entry, { bool enter = false, bool update = false, bool leave = false, @@ -430,97 +493,93 @@ Future testRealtimeEnterUpdateLeave(FlutterDriver driver) async { ); } - final clientIDClashMatrix = - transform(response.payload['clientIDClashMatrix']); - for (final clashEntry in clientIDClashMatrix) { - final realtimeClientID = clashEntry['realtimeClientId']; - final presenceClientID = clashEntry['presenceClientId']; - - if (realtimeClientID != presenceClientID) { - if (realtimeClientID == null) { - // only presenceClientID is present - testMatrixEntry( - clashEntry, - enterClient: true, - updateClient: true, - leaveClient: true, - ); - } else if (presenceClientID == null) { - // only realtimeClientID is present - testMatrixEntry( - clashEntry, - enter: true, - update: true, - leave: true, - ); - } else { - // both clientIDs are present and are unequal - testMatrixEntry( - clashEntry, - enter: true, - update: true, - leave: true, - ); - } - } else { - if (presenceClientID == null) { - // both clientIDs are equal and null - testMatrixEntry(clashEntry); + test( + 'clientID should be same in both realtime ClientOptions as well as' + ' the one passed to presence enter/update/leave APIs.' + ' If unequal, throw error.', () { + for (final clashEntry in clientIDClashMatrix) { + final realtimeClientID = clashEntry['realtimeClientId'] as String; + final presenceClientID = clashEntry['presenceClientId'] as String; + + if (realtimeClientID != presenceClientID) { + if (realtimeClientID == null) { + // only presenceClientID is present + testMatrixEntry( + clashEntry, + enterClient: true, + updateClient: true, + leaveClient: true, + ); + } else if (presenceClientID == null) { + // only realtimeClientID is present + testMatrixEntry( + clashEntry, + enter: true, + update: true, + leave: true, + ); + } else { + // both clientIDs are present and are unequal + testMatrixEntry( + clashEntry, + enter: true, + update: true, + leave: true, + ); + } } else { - // both clientIDs are equal and not null - testMatrixEntry( - clashEntry, - enter: true, - update: true, - leave: true, - enterClient: true, - updateClient: true, - leaveClient: true, - ); + if (presenceClientID == null) { + // both clientIDs are equal and null + testMatrixEntry(clashEntry); + } else { + // both clientIDs are equal and not null + testMatrixEntry( + clashEntry, + enter: true, + update: true, + leave: true, + enterClient: true, + updateClient: true, + leaveClient: true, + ); + } } } - } - - final actionMatrix = transform(response.payload['actionMatrix']); - for (final actionEntry in actionMatrix) { - // all cases should succeed - testMatrixEntry( - actionEntry, - enter: true, - update: true, - leave: true, - enterClient: true, - updateClient: true, - leaveClient: true, - ); - } + }); + + test('supports all data types as payload for presence actions', () { + for (final actionEntry in actionMatrix) { + testMatrixEntry( + actionEntry, + enter: true, + update: true, + leave: true, + enterClient: true, + updateClient: true, + leaveClient: true, + ); + } + }); } -Future testRealtimePresenceSubscription(FlutterDriver driver) async { +void testRealtimePresenceSubscription(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.realtimePresenceSubscribe); - - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['allMessages'], isA()); - expect(response.payload['enterMessages'], isA()); - expect(response.payload['enterUpdateMessages'], isA()); - expect(response.payload['partialMessages'], isA()); + TestControlMessage response; + List> allMessages; + List> enterMessages; + List> enterUpdateMessages; + List> partialMessages; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - final allMessages = transform(response.payload['allMessages']); - final enterMessages = transform(response.payload['enterMessages']); - final enterUpdateMessages = - transform(response.payload['enterUpdateMessages']); - final partialMessages = transform(response.payload['partialMessages']); - - expect(allMessages.length, equals(8)); - expect(enterMessages.length, equals(1)); - expect(enterUpdateMessages.length, equals(7)); - expect(partialMessages.length, equals(7)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + allMessages = transform(response.payload['allMessages']); + enterMessages = transform(response.payload['enterMessages']); + enterUpdateMessages = transform(response.payload['enterUpdateMessages']); + partialMessages = transform(response.payload['partialMessages']); + }); void _test(List> messages) { for (var i = 0; i < messages.length; i++) { @@ -534,8 +593,23 @@ Future testRealtimePresenceSubscription(FlutterDriver driver) async { } } - _test(allMessages); - _test(enterMessages); - _test(enterUpdateMessages); - expect(partialMessages, equals(enterUpdateMessages)); + test('listens to messages', () { + expect(allMessages.length, equals(8)); + _test(allMessages); + }); + + test('filters messages with single action', () { + expect(enterMessages.length, equals(1)); + _test(enterMessages); + }); + + test('filters messages with multiple actions', () { + expect(enterUpdateMessages.length, equals(7)); + _test(enterUpdateMessages); + }); + + test('listens to messages only until subscription is active', () { + expect(partialMessages.length, equals(7)); + expect(partialMessages, equals(enterUpdateMessages)); + }); } diff --git a/test_integration/test_driver/test_implementation/rest_tests.dart b/test_integration/test_driver/test_implementation/rest_tests.dart index 7a8cea4af..d8f2fa4c8 100644 --- a/test_integration/test_driver/test_implementation/rest_tests.dart +++ b/test_integration/test_driver/test_implementation/rest_tests.dart @@ -4,182 +4,388 @@ import 'package:test/test.dart'; import 'utils.dart'; -Future testRestPublish(FlutterDriver driver) async { +void testRestPublish(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.restPublish); + TestControlMessage response; + setUpAll(() async => response = await getTestResponse(getDriver(), message)); - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); + test('rest instance has a valid handle on publish', () { + expect(response.payload['handle'], isA()); + expect(response.payload['handle'], greaterThan(0)); + }); +} - // TODO(tiholic) enable this after implementing logger - // expect(response.payload['log'], isNotEmpty); +void testRestPublishSpec(FlutterDriver Function() getDriver) { + const message = TestControlMessage(TestName.restPublishSpec); + TestControlMessage response; + List messages; + List messages2; + List messages3; + Map exception; + Map exception2; + Map exception3; + + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + + messages = response.payload['publishedMessages'] as List; + messages2 = response.payload['publishedMessages2'] as List; + messages3 = response.payload['publishedMessages3'] as List; + exception = response.payload['exception'] as Map; + exception2 = response.payload['exception2'] as Map; + exception3 = response.payload['exception3'] as Map; + }); + + test('publishes without a name and data', () { + expect(messages[0]['name'], null); + expect(messages[0]['data'], null); + }); + test('publishes without data', () { + expect(messages[1]['name'], 'name1'); + expect(messages[1]['data'], null); + }); + test('publishes without a name', () { + expect(messages[2]['name'], null); + expect(messages[2]['data'], 'data1'); + }); + test('publishes with name and data', () { + expect(messages[3]['name'], 'name1'); + expect(messages[3]['data'], 'data1'); + }); + test('publishes single message object', () { + expect(messages[4]['name'], 'message-name1'); + expect(messages[4]['data'], 'message-data1'); + }); + test('publishes multiple message objects', () { + expect(messages[5]['name'], 'messages-name1'); + expect(messages[5]['data'], 'messages-data1'); + expect(messages[6]['name'], 'messages-name2'); + expect(messages[6]['data'], 'messages-data2'); + }); + test('publishes multiple messages at once', () { + expect(messages[5]['timestamp'] == messages[6]['timestamp'], true); + expect(messages[5]['timestamp'] != messages[4]['timestamp'], true); + }); + + // TODO(tiholic) Fails on iOS, track at https://github.com/ably/ably-cocoa/issues/1108 + test( + '(RSL1m1) Publishing a Message with no clientId when the clientId' + ' is set to some value in the client options should result in a message' + ' received with the clientId property set to that value', + () { + expect(messages[0]['clientId'], 'someClientId'); + }, + ); + + test( + '(RSL1m2) Publishing a Message with a clientId set to the same' + ' value as the clientId in the client options should result in' + ' a message received with the clientId property set to that value', () { + expect(messages[7]['clientId'], 'someClientId'); + }); + + test( + '(RSL1d) Raises an error if the message was not successfully published', + () => expect(exception == null, false), + ); + + test( + '(RSL1m4) Publishing a Message with a clientId set to a different value' + ' from the clientId in the client options should result in a message' + ' being rejected by the server.', + () { + expect(response.payload['exception'], isA()); + // TODO as error details are incompatible from both libraries, + // it makes no sense to include below expect's + // + // final exception = Map.castFrom( + // response.payload['exception'] as Map); + // expect(exception['code'], '14'); //40012 from android and 14 from iOS + // expect( + // exception['message'], + // 'Error publishing rest message;' + // ' err = attempted to publish message with an invalid clientId', + // ); + // expect(exception['errorInfo']['message'], + // 'attempted to publish message with an invalid clientId'); + }, + ); + + test( + '(RSL1m3) Publishing a Message with a clientId set to a value' + ' from an unidentified client (no clientId in the client options' + ' and credentials that can assume any clientId) should result in' + ' a message received with the clientId property set to that value', + () => expect(messages2[0]['clientId'], 'client-id'), + ); + + test( + '(RSL1i) If the total size of the message or (if publishing an array)' + ' messages, calculated per TO3l8, exceeds the maxMessageSize, then the' + ' client library should reject the publish and indicate an error' + ' with code 40009', + () { + // allows publishing messages length <= max allowed limit + expect(exception2 == null, true); + // errors out publishing messages length > max allowed limit + expect(exception3 == null, false); + }, + ); + + test( + 'publishes non-ascii characters', + () { + expect(messages3[0]['name'], 'Ωπ'); + expect(messages3[0]['data'], 'ΨΔ'); + }, + ); } -Future testRestPublishWithAuthCallback(FlutterDriver driver) async { +void testRestPublishWithAuthCallback(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.restPublishWithAuthCallback); + TestControlMessage response; + setUpAll(() async => response = await getTestResponse(getDriver(), message)); - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); - - expect(response.payload['authCallbackInvoked'], isTrue); + test('auth callback is invoked', () { + expect(response.payload['authCallbackInvoked'], isTrue); + }); } -Future testRestHistory(FlutterDriver driver) async { +void testRestHistory(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.restHistory); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); - - final paginatedResult = - response.payload['paginatedResult'] as Map; + Map paginatedResult; + List> historyDefault; + List> historyLimit4; + List> historyLimit2; + List> historyForwardLimit4; + List> historyWithStart; + List> historyWithStartAndEnd; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - final historyDefault = transform(response.payload['historyDefault']); - final historyLimit4 = transform(response.payload['historyLimit4']); - final historyLimit2 = transform(response.payload['historyLimit2']); - final historyForwardLimit4 = - transform(response.payload['historyForwardLimit4']); - final historyWithStart = transform(response.payload['historyWithStart']); - final historyWithStartAndEnd = - transform(response.payload['historyWithStartAndEnd']); - - expect(paginatedResult['hasNext'], false); - expect(paginatedResult['isLast'], true); - expect(paginatedResult['items'], isA()); - - expect(historyDefault.length, equals(8)); - expect(historyLimit4.length, equals(8)); - expect(historyLimit2.length, equals(8)); - expect(historyForwardLimit4.length, equals(8)); - expect(historyWithStart.length, equals(2)); - expect(historyWithStartAndEnd.length, equals(1)); - - testAllPublishedMessages(historyDefault.reversed.toList()); - testAllPublishedMessages(historyLimit4.reversed.toList()); - testAllPublishedMessages(historyLimit2.reversed.toList()); - testAllPublishedMessages(historyForwardLimit4); - - // start and no-end test (backward) - expect(historyWithStart[0]['name'], equals('history')); - expect(historyWithStart[0]['data'], equals('test2')); - - expect(historyWithStart[1]['name'], equals('history')); - expect(historyWithStart[1]['data'], equals('test')); - - // start and end test - expect(historyWithStartAndEnd[0]['name'], equals('history')); - expect(historyWithStartAndEnd[0]['data'], equals('test')); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + paginatedResult = + response.payload['paginatedResult'] as Map; + historyDefault = transform(response.payload['historyDefault']); + historyLimit4 = transform(response.payload['historyLimit4']); + historyLimit2 = transform(response.payload['historyLimit2']); + historyForwardLimit4 = transform(response.payload['historyForwardLimit4']); + historyWithStart = transform(response.payload['historyWithStart']); + historyWithStartAndEnd = transform( + response.payload['historyWithStartAndEnd'], + ); + }); + + group('paginated result', () { + test('#items is a list', () { + expect(paginatedResult['items'], isA()); + }); + test('#hasNext indicates whether there are more entries', () { + expect(paginatedResult['hasNext'], false); + }); + test('#isLast indicates if current page is last page', () { + expect(paginatedResult['isLast'], true); + }); + }); + + group('rest#channels#channel#history', () { + test('queries all entries by default', () { + expect(historyDefault.length, equals(8)); + testAllPublishedMessages(historyDefault.reversed.toList()); + }); + test('queries all entries by paginating with limit', () { + expect(historyLimit4.length, equals(8)); + expect(historyLimit2.length, equals(8)); + testAllPublishedMessages(historyLimit4.reversed.toList()); + testAllPublishedMessages(historyLimit2.reversed.toList()); + }); + test('queries entries in reverse order with direction set to "forward"', + () { + expect(historyForwardLimit4.length, equals(8)); + testAllPublishedMessages(historyForwardLimit4); + }); + test('returns entries created after specified time', () { + expect(historyWithStart.length, equals(2)); + expect(historyWithStart[0]['name'], equals('history')); + expect(historyWithStart[0]['data'], equals('test2')); + expect(historyWithStart[1]['name'], equals('history')); + expect(historyWithStart[1]['data'], equals('test')); + }); + test('returns entries created between specified start and end', () { + expect(historyWithStartAndEnd.length, equals(1)); + expect(historyWithStartAndEnd[0]['name'], equals('history')); + expect(historyWithStartAndEnd[0]['data'], equals('test')); + }); + }); } -Future testRestPresenceGet(FlutterDriver driver) async { +void testRestPresenceGet(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.restPresenceGet); + TestControlMessage response; - final response = await getTestResponse(driver, message); - - expect(response.testName, message.testName); - - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); + List> membersInitial; + List> membersDefault; + List> membersLimit4; + List> membersLimit2; + List> membersClientId; + List> membersConnectionId; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - int timestampSorter(Map a, Map b) { - if (DateTime.parse(a['timestamp'] as String).millisecondsSinceEpoch > - DateTime.parse(b['timestamp'] as String).millisecondsSinceEpoch) { - return 1; - } else { - return -1; - } - } - - final membersInitial = transform(response.payload['membersInitial']); - expect(membersInitial.length, equals(0)); - - final membersDefault = transform(response.payload['membersDefault']); - expect(membersDefault.length, equals(8)); - testAllPresenceMembers(membersDefault..sort(timestampSorter)); - - final membersLimit4 = transform(response.payload['membersLimit4']); - expect(membersLimit4.length, equals(8)); - testAllPresenceMembers(membersLimit4..sort(timestampSorter)); - - final membersLimit2 = transform(response.payload['membersLimit2']); - expect(membersLimit2.length, equals(8)); - testAllPresenceMembers(membersLimit2..sort(timestampSorter)); - - // there is only 1 client with clientId 'client-1 - final membersClientId = transform(response.payload['membersClientId']); - expect(membersClientId.length, equals(1)); - expect(membersClientId[0]['clientId'], equals('client-1')); - checkMessageData(1, membersClientId[0]['data']); - - // TODO similarly check for membersConnectionId after implementing - // connection id (sync from platform) on realtime connection - final membersConnectionId = - transform(response.payload['membersConnectionId']); - expect(membersConnectionId.length, equals(0)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + membersInitial = transform(response.payload['membersInitial']); + membersDefault = transform(response.payload['membersDefault']); + membersLimit4 = transform(response.payload['membersLimit4']); + membersLimit2 = transform(response.payload['membersLimit2']); + membersClientId = transform(response.payload['membersClientId']); + membersConnectionId = transform(response.payload['membersConnectionId']); + }); + + group('rest#channels#channel#presence#get', () { + test('has 0 members without any clients joined', () { + expect(membersInitial.length, equals(0)); + }); + test('queries all entries by default', () { + expect(membersDefault.length, equals(8)); + testAllPresenceMembers(membersDefault..sort(timestampSorter)); + }); + test('queries all entries by paginating with limit', () { + expect(membersLimit4.length, equals(8)); + expect(membersLimit2.length, equals(8)); + testAllPresenceMembers(membersLimit4..sort(timestampSorter)); + testAllPresenceMembers(membersLimit2..sort(timestampSorter)); + }); + test('filters entries with clientId when specified', () { + // there is only 1 client with clientId 'client-1 + expect(membersClientId.length, equals(1)); + expect(membersClientId[0]['clientId'], equals('client-1')); + checkMessageData(1, membersClientId[0]['data']); + }); + test('filters entries with clientId when specified', () { + // TODO similarly (like clientId) check for membersConnectionId + // after implementing connection id (sync from platform) + // on realtime connection + expect(membersConnectionId.length, equals(0)); + }); + }); } -Future testRestPresenceHistory(FlutterDriver driver) async { +void testRestPresenceHistory(FlutterDriver Function() getDriver) { const message = TestControlMessage(TestName.restPresenceHistory); + TestControlMessage response; - final response = await getTestResponse(driver, message); + List> transform(items) => + List.from(items as List).map((t) => t as Map).toList(); - expect(response.testName, message.testName); + List> historyInitial; + List> historyDefault; + List> historyLimit4; + List> historyLimit2; + List> historyForwards; + List> historyWithStart; + List> historyWithStartAndEnd; + List> historyAll; + + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + historyInitial = transform(response.payload['historyInitial']); + historyDefault = transform(response.payload['historyDefault']); + historyLimit4 = transform(response.payload['historyLimit4']); + historyLimit2 = transform(response.payload['historyLimit2']); + historyForwards = transform(response.payload['historyForwards']); + historyWithStart = transform( + response.payload['historyWithStart'], + ).reversed.toList(); + historyWithStartAndEnd = transform( + response.payload['historyWithStartAndEnd'], + ); + historyAll = transform(response.payload['historyAll']); + }); + + group('rest#channels#channel#presence#history', () { + test('queries all entries by default', () { + expect(historyInitial.length, equals(0)); + expect(historyAll.length, equals(10)); + + expect(historyDefault.length, equals(8)); + testAllPresenceMessagesHistory(historyDefault.reversed.toList()); + }); + test('queries all entries by paginating with limit', () { + expect(historyLimit4.length, equals(8)); + expect(historyLimit2.length, equals(8)); + testAllPresenceMessagesHistory(historyLimit4.reversed.toList()); + testAllPresenceMessagesHistory(historyLimit2.reversed.toList()); + }); + test( + 'queries entries in reverse order with direction set to "forward"', + () { + expect(historyForwards.length, equals(8)); + testAllPresenceMessagesHistory(historyForwards.toList()); + }, + ); + test('returns entries created after specified time', () { + expect(historyWithStart.length, equals(2)); + expect(historyWithStart[0]['clientId'], equals('someClientId')); + expect(historyWithStart[0]['data'], equals('enter-start-time')); + expect(historyWithStart[1]['clientId'], equals('someClientId')); + expect(historyWithStart[1]['data'], equals('leave-end-time')); + }); + test('returns entries created between specified start and end', () { + expect(historyWithStartAndEnd.length, equals(1)); + expect(historyWithStartAndEnd[0]['clientId'], equals('someClientId')); + expect(historyWithStartAndEnd[0]['data'], equals('enter-start-time')); + }); + }); +} - expect(response.payload['handle'], isA()); - expect(response.payload['handle'], greaterThan(0)); +void testCapabilityMatrix(FlutterDriver Function() getDriver) { + const message = TestControlMessage(TestName.restCapabilities); + TestControlMessage response; + List> capabilityMatrix; List> transform(items) => List.from(items as List).map((t) => t as Map).toList(); - final historyInitial = transform(response.payload['historyInitial']); - expect(historyInitial.length, equals(0)); - - final historyDefault = transform(response.payload['historyDefault']); - expect(historyDefault.length, equals(8)); - testAllPresenceMessagesHistory(historyDefault.reversed.toList()); - - final historyLimit4 = transform(response.payload['historyLimit4']); - expect(historyLimit4.length, equals(8)); - testAllPresenceMessagesHistory(historyLimit4.reversed.toList()); - - final historyLimit2 = transform(response.payload['historyLimit2']); - expect(historyLimit2.length, equals(8)); - testAllPresenceMessagesHistory(historyLimit2.reversed.toList()); - - final historyForwards = transform(response.payload['historyForwards']); - expect(historyForwards.length, equals(8)); - testAllPresenceMessagesHistory(historyForwards.toList()); - - final historyWithStart = - transform(response.payload['historyWithStart']).reversed.toList(); - expect(historyWithStart.length, equals(2)); - expect(historyWithStart[0]['clientId'], equals('someClientId')); - expect(historyWithStart[0]['data'], equals('enter-start-time')); - expect(historyWithStart[1]['clientId'], equals('someClientId')); - expect(historyWithStart[1]['data'], equals('leave-end-time')); - - final historyWithStartAndEnd = - transform(response.payload['historyWithStartAndEnd']); - expect(historyWithStartAndEnd.length, equals(1)); - expect(historyWithStartAndEnd[0]['clientId'], equals('someClientId')); - expect(historyWithStartAndEnd[0]['data'], equals('enter-start-time')); - - final historyAll = transform(response.payload['historyAll']); - expect(historyAll.length, equals(10)); + setUpAll(() async { + response = await getTestResponse(getDriver(), message); + capabilityMatrix = transform(response.payload['matrix']); + }); + + test('capabilitySpec', () { + for (final entry in capabilityMatrix) { + final capabilities = entry['channelCapabilities'] as List; + final publishEnabled = capabilities.contains('publish'); + final historyEnabled = capabilities.contains('history'); + final subscribeEnabled = capabilities.contains('subscribe'); + + final publishException = entry['publishException']; + final historyException = entry['historyException']; + final presenceException = entry['presenceException']; + final presenceHistoryException = entry['presenceHistoryException']; + + if (publishEnabled) { + expect(publishException == null, true); + } else { + expect(publishException['code'], '40160'); + } + if (historyEnabled) { + expect(historyException == null, true); + expect(presenceHistoryException == null, true); + } else { + expect(historyException['code'], '40160'); + expect(presenceHistoryException['code'], '40160'); + } + if (subscribeEnabled) { + expect(presenceException == null, subscribeEnabled); + } else { + expect(presenceException['code'], '40160'); + } + } + }); } diff --git a/test_integration/test_driver/test_implementation/utils.dart b/test_integration/test_driver/test_implementation/utils.dart index 519d272e7..1e03ea814 100644 --- a/test_integration/test_driver/test_implementation/utils.dart +++ b/test_integration/test_driver/test_implementation/utils.dart @@ -99,3 +99,12 @@ void testAllPresenceMessagesHistory( checkMessageData(i, history[i]['data']); } } + +int timestampSorter(Map a, Map b) { + if (DateTime.parse(a['timestamp'] as String).millisecondsSinceEpoch > + DateTime.parse(b['timestamp'] as String).millisecondsSinceEpoch) { + return 1; + } else { + return -1; + } +} diff --git a/test_integration/test_driver/tests_abstract.dart b/test_integration/test_driver/tests_abstract.dart index 127532879..e318b172a 100644 --- a/test_integration/test_driver/tests_abstract.dart +++ b/test_integration/test_driver/tests_abstract.dart @@ -15,33 +15,36 @@ void runTests({ groups: groups, ); - for (groupName in tests.keys) { - final name = - groupName.toString().substring(groupName.toString().indexOf('.') + 1); - group(name, () { - FlutterDriver driver; + FlutterDriver driver; + + // Connect to the Flutter driver before running any tests. + setUpAll(() async { + driver = await FlutterDriver.connect(printCommunication: true); + }); - // Connect to the Flutter driver before running any tests. - setUpAll(() async { - driver = await FlutterDriver.connect(printCommunication: true); - }); + tearDownAll(() async { + if (driver != null) { + const message = TestControlMessage(TestName.getFlutterErrors); + final flutterErrors = await getTestResponse(driver, message); + print('Flutter errors: ${flutterErrors.payload}'); + final _ = driver.close(); + } + }); - tearDownAll(() async { - if (driver != null) { - const message = TestControlMessage(TestName.getFlutterErrors); - final flutterErrors = await getTestResponse(driver, message); - print('Flutter errors: ${flutterErrors.payload}'); - final _ = driver.close(); - } - }); + FlutterDriver getDriver() => driver; - tests[groupName].forEach((testName, testFunction) { - test( - testName, - () => testFunction(driver), - timeout: const Timeout(Duration(minutes: 2)), - ); - }); + for (groupName in tests.keys) { + final name = + groupName.toString().substring(groupName.toString().indexOf('.') + 1); + tests[groupName].forEach(( + testName, + void Function(FlutterDriver Function()) testFunction, + ) { + group( + '$name $testName', + () => testFunction(getDriver), + timeout: const Timeout(Duration(minutes: 2)), + ); }); } } diff --git a/test_integration/test_driver/tests_config.dart b/test_integration/test_driver/tests_config.dart index 200f5772b..1cf4423ac 100644 --- a/test_integration/test_driver/tests_config.dart +++ b/test_integration/test_driver/tests_config.dart @@ -1,3 +1,5 @@ +import 'package:flutter_driver/flutter_driver.dart'; + import 'test_implementation/basic_platform_tests.dart'; import 'test_implementation/helper_tests.dart'; import 'test_implementation/realtime_tests.dart'; @@ -5,35 +7,38 @@ import 'test_implementation/rest_tests.dart'; enum TestGroup { basicTests, helperTests, rest, realtime } -final _tests = >{ +final _tests = + >{ TestGroup.basicTests: { 'should return Platform and Ably version': testPlatformAndAblyVersion, 'should provision AppKey': testDemoDependencies, }, + TestGroup.helperTests: { + 'should report unhandled exception': testShouldReportUnhandledException, + }, TestGroup.rest: { 'should publish': testRestPublish, 'should retrieve history': testRestHistory, + 'conforms to publish spec RSl1': testRestPublishSpec, 'should publish with AuthCallback': testRestPublishWithAuthCallback, 'should get Presence Members': testRestPresenceGet, 'should get Presence History': testRestPresenceHistory, + 'conforms to capabilitySpec': testCapabilityMatrix, }, TestGroup.realtime: { - 'should publish': testRealtimePublish, + 'realtime#channels#channel#publish': testRealtimePublish, 'should subscribe to connection and channel': testRealtimeEvents, 'should subscribe to messages': testRealtimeSubscribe, 'should retrieve history': testRealtimeHistory, - 'should publish with authCallback': testRealtimePublishWithAuthCallback, - 'should get Presence Members': testRealtimePresenceGet, - 'should get Presence History': testRealtimePresenceHistory, + 'realtime#channels#channel#presence#get': testRealtimePresenceGet, + 'realtime#channels#channel#presence#history': testRealtimePresenceHistory, 'should enter, update and leave Presence': testRealtimeEnterUpdateLeave, 'should subscribe to channel presence': testRealtimePresenceSubscription, - }, - TestGroup.helperTests: { - 'should report unhandled exception': testShouldReportUnhandledException, } }; -Map> getTestsFor({ +Map> + getTestsFor({ bool all = false, TestGroup group, List groups,