diff --git a/packages/at_lookup/CHANGELOG.md b/packages/at_lookup/CHANGELOG.md index 51f2f642..e6bef4fb 100644 --- a/packages/at_lookup/CHANGELOG.md +++ b/packages/at_lookup/CHANGELOG.md @@ -1,3 +1,5 @@ +## 4.0.0 +- feat: Introduce websocket support in at_lookup_impl ## 3.0.49 - build[deps]: Upgraded the following packages: - at_commons to v5.0.0 diff --git a/packages/at_lookup/lib/at_lookup.dart b/packages/at_lookup/lib/at_lookup.dart index b013990c..47fd67c4 100644 --- a/packages/at_lookup/lib/at_lookup.dart +++ b/packages/at_lookup/lib/at_lookup.dart @@ -5,10 +5,9 @@ library at_lookup; export 'src/at_lookup.dart'; export 'src/at_lookup_impl.dart'; -export 'src/connection/outbound_connection.dart'; -export 'src/connection/outbound_connection_impl.dart'; export 'src/exception/at_lookup_exception.dart'; export 'src/monitor_client.dart'; export 'src/cache/secondary_address_finder.dart'; export 'src/cache/cacheable_secondary_address_finder.dart'; export 'src/util/secure_socket_util.dart'; +export 'src/connection/at_lookup_connection_factory.dart'; \ No newline at end of file diff --git a/packages/at_lookup/lib/src/at_lookup_impl.dart b/packages/at_lookup/lib/src/at_lookup_impl.dart index 550c1801..1dad843d 100644 --- a/packages/at_lookup/lib/src/at_lookup_impl.dart +++ b/packages/at_lookup/lib/src/at_lookup_impl.dart @@ -5,25 +5,28 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:at_chops/at_chops.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; -import 'package:at_lookup/src/connection/outbound_message_listener.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_lookup/src/connection/at_message_listener.dart'; import 'package:at_utils/at_logger.dart'; import 'package:crypto/crypto.dart'; import 'package:crypton/crypton.dart'; import 'package:mutex/mutex.dart'; -import 'package:at_chops/at_chops.dart'; class AtLookupImpl implements AtLookUp { final logger = AtSignLogger('AtLookup'); /// Listener for reading verb responses from the remote server - late OutboundMessageListener messageListener; + late AtMessageListener messageListener; - OutboundConnection? _connection; + AtConnection? _connection; // Represents Socket or WebSocket connection - OutboundConnection? get connection => _connection; + AtConnection? get connection => _connection; + + late AtLookupConnectionFactory atConnectionFactory; @override late SecondaryAddressFinder secondaryAddressFinder; @@ -40,16 +43,10 @@ class AtLookupImpl implements AtLookUp { String? cramSecret; // ignore: prefer_typing_uninitialized_variables - var outboundConnectionTimeout; + var atConnectionTimeout; late SecureSocketConfig _secureSocketConfig; - late final AtLookupSecureSocketFactory socketFactory; - - late final AtLookupSecureSocketListenerFactory socketListenerFactory; - - late AtLookupOutboundConnectionFactory outboundConnectionFactory; - /// Represents the client configurations. late Map _clientConfig; @@ -61,9 +58,9 @@ class AtLookupImpl implements AtLookUp { SecondaryAddressFinder? secondaryAddressFinder, SecureSocketConfig? secureSocketConfig, Map? clientConfig, - AtLookupSecureSocketFactory? secureSocketFactory, - AtLookupSecureSocketListenerFactory? socketListenerFactory, - AtLookupOutboundConnectionFactory? outboundConnectionFactory}) { + AtLookupConnectionFactory? atConnectionFactory}) { + // Default to secure socket factory + this.atConnectionFactory = atConnectionFactory ?? AtLookupSecureSocketFactory(); _currentAtSign = atSign; _rootDomain = rootDomain; _rootPort = rootPort; @@ -73,11 +70,6 @@ class AtLookupImpl implements AtLookUp { // Stores the client configurations. // If client configurations are not available, defaults to empty map _clientConfig = clientConfig ?? {}; - socketFactory = secureSocketFactory ?? AtLookupSecureSocketFactory(); - this.socketListenerFactory = - socketListenerFactory ?? AtLookupSecureSocketListenerFactory(); - this.outboundConnectionFactory = - outboundConnectionFactory ?? AtLookupOutboundConnectionFactory(); } @Deprecated('use CacheableSecondaryAddressFinder') @@ -246,16 +238,17 @@ class AtLookupImpl implements AtLookUp { await _connection!.close(); } logger.info('Creating new connection'); - //1. find secondary url for atsign from lookup library + + // 1. Find secondary URL for the atsign from the lookup library SecondaryAddress secondaryAddress = await secondaryAddressFinder.findSecondary(_currentAtSign); var host = secondaryAddress.host; var port = secondaryAddress.port; - //2. create a connection to secondary server - await createOutBoundConnection( - host, port.toString(), _currentAtSign, _secureSocketConfig); - //3. listen to server response - messageListener = socketListenerFactory.createListener(_connection!); + + // 2. Create a connection to the secondary server + await createAtConnection(host, port.toString(), _secureSocketConfig); + + // 3. Listen to server response messageListener.listen(); logger.info('New connection created OK'); } @@ -436,7 +429,7 @@ class AtLookupImpl implements AtLookUp { await createConnection(); try { await _pkamAuthenticationMutex.acquire(); - if (!_connection!.getMetaData()!.isAuthenticated) { + if (!_connection!.metaData.isAuthenticated) { await _sendCommand((FromVerbBuilder() ..atSign = _currentAtSign ..clientConfig = _clientConfig) @@ -458,13 +451,13 @@ class AtLookupImpl implements AtLookUp { var pkamResponse = await messageListener.read(); if (pkamResponse == 'data:success') { logger.info('auth success'); - _connection!.getMetaData()!.isAuthenticated = true; + _connection!.metaData.isAuthenticated = true; } else { throw UnAuthenticatedException( 'Failed connecting to $_currentAtSign. $pkamResponse'); } } - return _connection!.getMetaData()!.isAuthenticated; + return _connection!.metaData.isAuthenticated; } finally { _pkamAuthenticationMutex.release(); } @@ -475,7 +468,7 @@ class AtLookupImpl implements AtLookUp { await createConnection(); try { await _pkamAuthenticationMutex.acquire(); - if (!_connection!.getMetaData()!.isAuthenticated) { + if (!_connection!.metaData.isAuthenticated) { await _sendCommand((FromVerbBuilder() ..atSign = _currentAtSign ..clientConfig = _clientConfig) @@ -505,13 +498,13 @@ class AtLookupImpl implements AtLookUp { var pkamResponse = await messageListener.read(); if (pkamResponse == 'data:success') { logger.info('auth success'); - _connection!.getMetaData()!.isAuthenticated = true; + _connection!.metaData.isAuthenticated = true; } else { throw UnAuthenticatedException( 'Failed connecting to $_currentAtSign. $pkamResponse'); } } - return _connection!.getMetaData()!.isAuthenticated; + return _connection!.metaData.isAuthenticated; } finally { _pkamAuthenticationMutex.release(); } @@ -524,32 +517,41 @@ class AtLookupImpl implements AtLookUp { await createConnection(); try { await _cramAuthenticationMutex.acquire(); - if (!_connection!.getMetaData()!.isAuthenticated) { + + if (!_connection!.metaData.isAuthenticated) { + // Use the connection and message listener dynamically await _sendCommand((FromVerbBuilder() ..atSign = _currentAtSign ..clientConfig = _clientConfig) .buildCommand()); + var fromResponse = await messageListener.read( transientWaitTimeMillis: 4000, maxWaitMilliSeconds: 10000); logger.info('from result:$fromResponse'); + if (fromResponse.isEmpty) { return false; } + fromResponse = fromResponse.trim().replaceAll('data:', ''); + var digestInput = '$secret$fromResponse'; var bytes = utf8.encode(digestInput); var digest = sha512.convert(bytes); + await _sendCommand('cram:$digest\n'); var cramResponse = await messageListener.read( transientWaitTimeMillis: 4000, maxWaitMilliSeconds: 10000); + if (cramResponse == 'data:success') { logger.info('auth success'); - _connection!.getMetaData()!.isAuthenticated = true; + _connection!.metaData.isAuthenticated = true; } else { throw UnAuthenticatedException('Auth failed'); } } - return _connection!.getMetaData()!.isAuthenticated; + + return _connection!.metaData.isAuthenticated; } finally { _cramAuthenticationMutex.release(); } @@ -624,23 +626,30 @@ class AtLookupImpl implements AtLookUp { } bool _isAuthRequired() { - return !isConnectionAvailable() || - !(_connection!.getMetaData()!.isAuthenticated); + return !isConnectionAvailable() || !(_connection!.metaData.isAuthenticated); } - Future createOutBoundConnection(String host, String port, - String toAtSign, SecureSocketConfig secureSocketConfig) async { + Future createAtConnection( + String host, String port, SecureSocketConfig secureSocketConfig) async { try { - SecureSocket secureSocket = - await socketFactory.createSocket(host, port, secureSocketConfig); - _connection = - outboundConnectionFactory.createOutboundConnection(secureSocket); - if (outboundConnectionTimeout != null) { - _connection!.setIdleTime(outboundConnectionTimeout); + // Create the socket connection using the factory + final underlying = await atConnectionFactory.createUnderlying( + host, port, secureSocketConfig); + + // Create at connection and listener using the factory's methods + AtConnection atConnection = + atConnectionFactory.createConnection(underlying); + messageListener = atConnectionFactory.createListener(atConnection); + + _connection = atConnection; + + // Set idle time if applicable + if (atConnectionTimeout != null) { + atConnection.setIdleTime(atConnectionTimeout); } } on SocketException { throw SecondaryConnectException( - 'unable to connect to secondary $toAtSign on $host:$port'); + 'Unable to connect to secondary $_currentAtSign on $host:$port'); } return true; } @@ -682,23 +691,3 @@ class AtLookupImpl implements AtLookUp { @override String? enrollmentId; } - -class AtLookupSecureSocketFactory { - Future createSocket( - String host, String port, SecureSocketConfig socketConfig) async { - return await SecureSocketUtil.createSecureSocket(host, port, socketConfig); - } -} - -class AtLookupSecureSocketListenerFactory { - OutboundMessageListener createListener( - OutboundConnection outboundConnection) { - return OutboundMessageListener(outboundConnection); - } -} - -class AtLookupOutboundConnectionFactory { - OutboundConnection createOutboundConnection(SecureSocket secureSocket) { - return OutboundConnectionImpl(secureSocket); - } -} diff --git a/packages/at_lookup/lib/src/cache/cacheable_secondary_address_finder.dart b/packages/at_lookup/lib/src/cache/cacheable_secondary_address_finder.dart index dc74cef9..04730ace 100644 --- a/packages/at_lookup/lib/src/cache/cacheable_secondary_address_finder.dart +++ b/packages/at_lookup/lib/src/cache/cacheable_secondary_address_finder.dart @@ -128,10 +128,10 @@ class SecondaryAddressCacheEntry { class SecondaryUrlFinder { final String _rootDomain; final int _rootPort; - late final AtLookupSecureSocketFactory _socketFactory; + late final AtLookupConnectionFactory _socketFactory; SecondaryUrlFinder(this._rootDomain, this._rootPort, - {AtLookupSecureSocketFactory? socketFactory}) { + {AtLookupConnectionFactory? socketFactory}) { _socketFactory = socketFactory ?? AtLookupSecureSocketFactory(); } @@ -188,11 +188,11 @@ class SecondaryUrlFinder { var prompt = false; var once = true; - socket = await _socketFactory.createSocket( + socket = await _socketFactory.createUnderlying( _rootDomain, '$_rootPort', SecureSocketConfig()); _logger.finer('findSecondaryUrl: connection to root server established'); // listen to the received data event stream - socket.listen((List event) async { + socket!.listen((List event) async { _logger.finest('root socket listener received: $event'); answer = utf8.decode(event); diff --git a/packages/at_lookup/lib/src/connection/at_connection.dart b/packages/at_lookup/lib/src/connection/at_connection.dart index dd2d8d84..55de3897 100644 --- a/packages/at_lookup/lib/src/connection/at_connection.dart +++ b/packages/at_lookup/lib/src/connection/at_connection.dart @@ -1,25 +1,55 @@ -import 'dart:io'; +import 'dart:async'; -abstract class AtConnection { - /// Write a data to the underlying socket of the connection +abstract class AtConnection { + /// The underlying connection + T get underlying; + + /// Metadata for the connection + final AtConnectionMetaData metaData = AtConnectionMetaData(); + + /// The idle timeout in milliseconds (default: 10 minutes) + int idleTimeMillis = 600000; + + AtConnection() { + metaData.created = DateTime.now().toUtc(); + } + + /// Writes data to the underlying socket of the connection. /// @param - data - Data to write to the socket /// @throws [AtIOException] for any exception during the operation - void write(String data); + FutureOr write(String data); - /// Retrieves the socket of underlying connection - Socket getSocket(); - - /// closes the underlying connection + /// Closes the underlying connection. Future close(); - /// Returns true if the connection is invalid - bool isInValid(); + /// Returns true if the connection is invalid. + bool isInValid() { + return _isIdle() || metaData.isClosed || metaData.isStale; + } + + /// Updates the idle time for the connection (Socket or WebSocket). + void setIdleTime(int? idleTimeMillis) { + if (idleTimeMillis != null) { + this.idleTimeMillis = idleTimeMillis; + } + } + + /// Checks if the connection has been idle for longer than the specified timeout. + bool _isIdle() { + return _getIdleTimeMillis() > idleTimeMillis; + } - /// Gets the connection metadata - AtConnectionMetaData? getMetaData(); + /// Calculates the idle time in milliseconds. + int _getIdleTimeMillis() { + var lastAccessedTime = metaData.lastAccessed; + lastAccessedTime ??= metaData.created; + var currentTime = DateTime.now().toUtc(); + return currentTime.difference(lastAccessedTime!).inMilliseconds; + } } -abstract class AtConnectionMetaData { +/// Metadata for [AtConnection]. +class AtConnectionMetaData { bool isAuthenticated = false; DateTime? lastAccessed; DateTime? created; diff --git a/packages/at_lookup/lib/src/connection/at_lookup_connection_factory.dart b/packages/at_lookup/lib/src/connection/at_lookup_connection_factory.dart new file mode 100644 index 00000000..bdf87b67 --- /dev/null +++ b/packages/at_lookup/lib/src/connection/at_lookup_connection_factory.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/at_lookup.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_lookup/src/connection/at_socket_connection.dart'; +import 'package:at_lookup/src/connection/at_websocket_connection.dart'; + +import 'at_message_listener.dart'; + +/// This factory is responsible for creating the underlying connection, +/// an connection wrapper, and the message listener for a +/// specific type of connection (e.g., `SecureSocket` or `WebSocket`). +abstract class AtLookupConnectionFactory { + /// Creates the underlying connection of type [T]. + Future createUnderlying( + String host, String port, SecureSocketConfig secureSocketConfig); + + /// Wraps the underlying connection of type [T] into an connection [U]. + U createConnection(T underlying); + + /// Creates an [AtMessageListener] to manage messages for the given [U] connection. + AtMessageListener createListener(U connection); +} + +/// Factory class to create a secure connection over [SecureSocket]. +class AtLookupSecureSocketFactory extends AtLookupConnectionFactory< + SecureSocket, AtConnection> { + /// Creates a secure socket connection to the specified [host] and [port] + /// using the given [secureSocketConfig]. Returns a [SecureSocket] + @override + Future createUnderlying( + String host, String port, SecureSocketConfig secureSocketConfig) async { + return await SecureSocketUtil.createSecureSocket( + host, port, secureSocketConfig); + } + + /// Wraps the [SecureSocket] connection into an [AtConnection] instance. + @override + AtConnection createConnection(SecureSocket underlying) { + return AtSocketConnection(underlying); + } + + /// Creates an [AtMessageListener] to manage messages for the secure + /// socket-based [AtConnection]. + @override + AtMessageListener createListener(AtConnection connection) { + return AtMessageListener(connection); + } +} + +/// Factory class to create a WebSocket-based connection. +class AtLookupWebSocketFactory extends AtLookupConnectionFactory< + WebSocket, AtConnection> { + /// Creates a WebSocket connection to the specified [host] and [port] + /// using the given [secureSocketConfig]. + @override + Future createUnderlying( + String host, String port, SecureSocketConfig secureSocketConfig) async { + final socket = await SecureSocketUtil.createSecureSocket( + host, port, secureSocketConfig, + isWebSocket: true); + return socket as WebSocket; + } + + /// Wraps the [WebSocket] connection into an [AtConnection] instance. + @override + AtConnection createConnection(underlying) { + return AtWebSocketConnection(underlying); + } + + /// Creates an [AtMessageListener] to manage messages for the + /// WebSocket-based [AtConnection]. + @override + AtMessageListener createListener( + AtConnection connection) { + return AtMessageListener(connection); + } +} diff --git a/packages/at_lookup/lib/src/connection/outbound_message_listener.dart b/packages/at_lookup/lib/src/connection/at_message_listener.dart similarity index 84% rename from packages/at_lookup/lib/src/connection/outbound_message_listener.dart rename to packages/at_lookup/lib/src/connection/at_message_listener.dart index 81d9d91a..52813510 100644 --- a/packages/at_lookup/lib/src/connection/outbound_message_listener.dart +++ b/packages/at_lookup/lib/src/connection/at_message_listener.dart @@ -8,8 +8,8 @@ import 'package:at_lookup/src/connection/at_connection.dart'; import 'package:at_utils/at_logger.dart'; import 'package:meta/meta.dart'; -///Listener class for messages received by [RemoteSecondary] -class OutboundMessageListener { +/// Listener class for messages received by [RemoteSecondary] +class AtMessageListener { final logger = AtSignLogger('OutboundMessageListener'); late ByteBuffer _buffer; final Queue _queue = Queue(); @@ -19,20 +19,25 @@ class OutboundMessageListener { final int atCharCodeUnit = 64; late DateTime _lastReceivedTime; - OutboundMessageListener(this._connection, {int bufferCapacity = 10240000}) { + AtMessageListener(this._connection, {int bufferCapacity = 10240000}) { _buffer = ByteBuffer(capacity: bufferCapacity); } - /// Listens to the underlying connection's socket if the connection is created. + /// Listens to the underlying connection's socket (either WebSocket or raw socket) + /// if the connection is created. /// @throws [AtConnectException] if the connection is not yet created void listen() { logger.finest('Calling socket.listen within runZonedGuarded block'); runZonedGuarded(() { - _connection - .getSocket() - .listen(messageHandler, onDone: onSocketDone, onError: onSocketError); + final stream = _connection.underlying as Stream; + stream.listen( + messageHandler, + onDone: onSocketDone, + onError: onSocketError, + ); }, (Object error, StackTrace st) { + logger.finer('stack trace $st'); logger.warning( 'runZonedGuarded received socket error $error - calling onSocketError() to close connection'); onSocketError(error); @@ -62,13 +67,23 @@ class OutboundMessageListener { /// Handles messages on the inbound client's connection and calls the verb executor /// Closes the inbound connection in case of any error. - /// Throw a [BufferOverFlowException] if buffer is unable to hold incoming data - Future messageHandler(List data) async { + void messageHandler(dynamic data) { String result; int offset; _lastReceivedTime = DateTime.now(); - // check buffer overflow + + // If data is a String (from WebSocket), process it directly as UTF-8 encoded text. + if (data is String) { + logger.finer('WebSocket received string data: $data'); + data = utf8.encode(data); // Convert the WebSocket message to byte array + } else if (data is! List) { + logger.warning('Received unexpected data type: ${data.runtimeType}'); + return; // Exit if the data is neither String nor List + } + + // At this point, data is guaranteed to be List for both WebSocket and raw socket _checkBufferOverFlow(data); + // If the data contains a new line character, add until the new line char to buffer if (data.contains(newLineCodeUnit)) { offset = data.lastIndexOf(newLineCodeUnit); @@ -93,7 +108,7 @@ class OutboundMessageListener { result = _stripPrompt(result); logger.finer('RECEIVED $result'); _queue.add(result); - //clear the buffer after adding result to queue + // Clear the buffer after adding result to queue _buffer.clear(); _buffer.addByte(data[element]); } else { @@ -105,9 +120,9 @@ class OutboundMessageListener { /// The methods verifies if buffer has the capacity to accept the data. /// /// Throw BufferOverFlowException if data length exceeds the buffer capacity - _checkBufferOverFlow(data) { + void _checkBufferOverFlow(List data) { if (_buffer.isOverFlow(data)) { - int bufferLength = (_buffer.length() + data.length) as int; + int bufferLength = (_buffer.length() + data.length); _buffer.clear(); throw BufferOverFlowException( 'data length exceeded the buffer limit. Data length : $bufferLength and Buffer capacity ${_buffer.capacity}'); @@ -166,7 +181,7 @@ class OutboundMessageListener { _buffer.clear(); await closeConnection(); throw AtTimeoutException( - 'Waited for $transientWaitTimeMillis millis. No response after $_lastReceivedTime '); + 'Waited for $transientWaitTimeMillis millis. No response after $_lastReceivedTime'); } // wait for 10 ms before attempting to read from queue again await Future.delayed(Duration(milliseconds: 10)); diff --git a/packages/at_lookup/lib/src/connection/at_socket_connection.dart b/packages/at_lookup/lib/src/connection/at_socket_connection.dart new file mode 100644 index 00000000..9f6df5c4 --- /dev/null +++ b/packages/at_lookup/lib/src/connection/at_socket_connection.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_utils/at_logger.dart'; + +/// Base class for common socket operations +class AtSocketConnection extends AtConnection { + final T _socket; + final AtSignLogger logger = AtSignLogger('AtSocketConnection'); + final StringBuffer buffer = StringBuffer(); + + AtSocketConnection(this._socket) { + _socket.setOption(SocketOption.tcpNoDelay, true); + metaData.created = DateTime.now().toUtc(); + } + + @override + Future close() async { + if (metaData.isClosed) { + logger.finer('close(): connection is already closed'); + return; + } + + try { + var address = underlying.remoteAddress; + var port = underlying.remotePort; + + logger.info( + 'close(): calling socket.destroy() on connection to $address:$port'); + underlying.destroy(); + } catch (e) { + // Ignore errors or exceptions on a connection close + logger.finer('Exception "$e" while destroying socket - ignoring'); + metaData.isStale = true; + } finally { + metaData.isClosed = true; + } + } + + @override + T get underlying => _socket; + + @override + FutureOr write(String data) async { + if (isInValid()) { + throw ConnectionInvalidException('write(): Connection is invalid'); + } + try { + underlying.write(data); + metaData.lastAccessed = DateTime.now().toUtc(); + } on Exception { + metaData.isStale = true; + } + } +} diff --git a/packages/at_lookup/lib/src/connection/at_websocket_connection.dart b/packages/at_lookup/lib/src/connection/at_websocket_connection.dart new file mode 100644 index 00000000..7ba3170e --- /dev/null +++ b/packages/at_lookup/lib/src/connection/at_websocket_connection.dart @@ -0,0 +1,55 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_utils/at_logger.dart'; + +/// WebSocket-specific connection class +class AtWebSocketConnection extends AtConnection { + final T _webSocket; + late final AtSignLogger logger; + StringBuffer? buffer; + + AtWebSocketConnection(this._webSocket) { + logger = AtSignLogger(runtimeType.toString()); + buffer = StringBuffer(); + metaData.created = DateTime.now().toUtc(); + } + + @override + Future close() async { + if (metaData.isClosed) { + logger.finer('close(): WebSocket connection is already closed'); + return; + } + + try { + logger.info('close(): closing WebSocket connection'); + await _webSocket.close(); + } catch (e) { + // Ignore errors or exceptions on connection close + logger.finer('Exception "$e" while closing WebSocket - ignoring'); + metaData.isStale = true; + } finally { + metaData.isClosed = true; + } + } + + @override + T get underlying => _webSocket; + + @override + FutureOr write(String data) async { + if (isInValid()) { + throw ConnectionInvalidException( + 'write(): WebSocket connection is invalid'); + } + + try { + _webSocket.add(data); // WebSocket uses add() to send data + metaData.lastAccessed = DateTime.now().toUtc(); + } on Exception { + metaData.isStale = true; + } + } +} diff --git a/packages/at_lookup/lib/src/connection/base_connection.dart b/packages/at_lookup/lib/src/connection/base_connection.dart deleted file mode 100644 index 0b6354a0..00000000 --- a/packages/at_lookup/lib/src/connection/base_connection.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:io'; - -import 'package:at_commons/at_commons.dart'; -import 'package:at_lookup/src/connection/at_connection.dart'; -import 'package:at_utils/at_logger.dart'; - -/// Base class for common socket operations -abstract class BaseConnection extends AtConnection { - late final AtSignLogger logger; - late final Socket _socket; - StringBuffer? buffer; - AtConnectionMetaData? metaData; - - BaseConnection(Socket? socket) { - logger = AtSignLogger(runtimeType.toString()); - buffer = StringBuffer(); - socket?.setOption(SocketOption.tcpNoDelay, true); - _socket = socket!; - } - - @override - AtConnectionMetaData? getMetaData() { - return metaData; - } - - @override - Future close() async { - if (getMetaData()!.isClosed) { - logger.finer('close(): connection is already closed'); - return; - } - - try { - var address = _socket.remoteAddress; - var port = _socket.remotePort; - - logger.info('close(): calling socket.destroy()' - ' on connection to $address:$port'); - _socket.destroy(); - } catch (e) { - // Ignore errors or exceptions on a connection close - logger.finer('Exception "$e" while destroying socket - ignoring'); - getMetaData()!.isStale = true; - } finally { - getMetaData()!.isClosed = true; - } - } - - @override - Socket getSocket() { - return _socket; - } - - @override - Future write(String data) async { - if (isInValid()) { - //# Replace with specific exception - throw ConnectionInvalidException('write(): Connection is invalid'); - } - try { - getSocket().write(data); - getMetaData()!.lastAccessed = DateTime.now().toUtc(); - } on Exception { - getMetaData()!.isStale = true; - } - } -} diff --git a/packages/at_lookup/lib/src/connection/outbound_connection.dart b/packages/at_lookup/lib/src/connection/outbound_connection.dart deleted file mode 100644 index 75a290d7..00000000 --- a/packages/at_lookup/lib/src/connection/outbound_connection.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'dart:io'; -import 'package:at_lookup/src/connection/at_connection.dart'; -import 'package:at_lookup/src/connection/base_connection.dart'; - -abstract class OutboundConnection extends BaseConnection { - OutboundConnection(Socket socket) : super(socket); - void setIdleTime(int? idleTimeMillis); -} - -/// Metadata information for [OutboundConnection] -class OutboundConnectionMetadata extends AtConnectionMetaData {} diff --git a/packages/at_lookup/lib/src/connection/outbound_connection_impl.dart b/packages/at_lookup/lib/src/connection/outbound_connection_impl.dart deleted file mode 100644 index 4a79e58c..00000000 --- a/packages/at_lookup/lib/src/connection/outbound_connection_impl.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:io'; -import 'outbound_connection.dart'; - -class OutboundConnectionImpl extends OutboundConnection { - int? outboundIdleTime = 600000; //default timeout 10 minutes - - OutboundConnectionImpl(Socket socket) : super(socket) { - metaData = OutboundConnectionMetadata()..created = DateTime.now().toUtc(); - } - - int _getIdleTimeMillis() { - var lastAccessedTime = getMetaData()!.lastAccessed; - lastAccessedTime ??= getMetaData()!.created; - var currentTime = DateTime.now().toUtc(); - return currentTime.difference(lastAccessedTime!).inMilliseconds; - } - - bool _isIdle() { - return _getIdleTimeMillis() > outboundIdleTime!; - } - - @override - bool isInValid() { - return _isIdle() || getMetaData()!.isClosed || getMetaData()!.isStale; - } - - @override - void setIdleTime(int? idleTimeMillis) { - outboundIdleTime = idleTimeMillis; - } -} diff --git a/packages/at_lookup/lib/src/monitor_client.dart b/packages/at_lookup/lib/src/monitor_client.dart index 49f3c3dd..1a8bd338 100644 --- a/packages/at_lookup/lib/src/monitor_client.dart +++ b/packages/at_lookup/lib/src/monitor_client.dart @@ -6,6 +6,8 @@ import 'dart:typed_data'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_lookup/src/connection/at_socket_connection.dart'; import 'package:at_utils/at_logger.dart'; import 'package:crypton/crypton.dart'; @@ -21,14 +23,14 @@ class MonitorClient { } ///Monitor Verb - Future executeMonitorVerb(String _command, String _atSign, + Future executeMonitorVerb(String _command, String _atSign, String _rootDomain, int _rootPort, Function notificationCallBack, {bool auth = true, Function? restartCallBack}) async { //1. Get a new outbound connection dedicated to monitor verb. var _monitorConnection = await _createNewConnection(_atSign, _rootDomain, _rootPort); //2. Listener on _monitorConnection. - _monitorConnection.getSocket().listen((event) { + _monitorConnection.underlying.listen((event) { response = utf8.decode(event); // If response contains data to be notified, invoke callback function. if (response.toString().startsWith('notification')) { @@ -49,7 +51,7 @@ class MonitorClient { } /// Create a new connection for monitor verb. - Future _createNewConnection( + Future _createNewConnection( String toAtSign, String rootDomain, int rootPort) async { //1. find secondary url for atsign from lookup library var secondaryUrl = @@ -61,14 +63,14 @@ class MonitorClient { //2. create a connection to secondary server var secureSocket = await SecureSocket.connect(host, int.parse(port)); - OutboundConnection _monitorConnection = - OutboundConnectionImpl(secureSocket); + AtConnection _monitorConnection = + AtSocketConnection(secureSocket); return _monitorConnection; } /// To authenticate connection via PKAM verb. - Future _authenticateConnection( - String _atSign, OutboundConnection _monitorConnection) async { + Future _authenticateConnection( + String _atSign, AtConnection _monitorConnection) async { await _monitorConnection.write('from:$_atSign\n'); var fromResponse = await _getQueueResponse(); logger.info('from result:$fromResponse'); @@ -123,16 +125,16 @@ class MonitorClient { } /// Logs the error and closes the [OutboundConnection] - Future _errorHandler(error, OutboundConnection _connection) async { + Future _errorHandler(error, AtConnection _connection) async { await _closeConnection(_connection); } - /// Closes the [OutboundConnection] - void _finishedHandler(OutboundConnection _connection) async { + /// Closes the [AtConnection] + void _finishedHandler(AtConnection _connection) async { await _closeConnection(_connection); } - Future _closeConnection(OutboundConnection _connection) async { + Future _closeConnection(AtConnection _connection) async { if (!_connection.isInValid()) { await _connection.close(); } diff --git a/packages/at_lookup/lib/src/util/secure_socket_util.dart b/packages/at_lookup/lib/src/util/secure_socket_util.dart index 6eff0733..3c8bbf55 100644 --- a/packages/at_lookup/lib/src/util/secure_socket_util.dart +++ b/packages/at_lookup/lib/src/util/secure_socket_util.dart @@ -1,15 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:at_commons/at_commons.dart'; +import 'package:at_utils/at_logger.dart'; class SecureSocketUtil { - ///method that creates and returns a [SecureSocket]. If [decryptPackets] is set to true,the TLS keys are logged into a file. - static Future createSecureSocket( + static final AtSignLogger logger = AtSignLogger('socketutil'); + + /// Method that creates and returns either a [SecureSocket] or a [WebSocket]. + /// If [decryptPackets] is set to true, the TLS keys are logged into a file. + static Future createSecureSocket( + String host, String port, SecureSocketConfig secureSocketConfig, + {bool isWebSocket = false}) async { + if (isWebSocket) { + return _createSecureWebSocket(host, port, secureSocketConfig); + } else { + return _createSecureSocket(host, port, secureSocketConfig); + } + } + + static Future _createSecureWebSocket( + String host, String port, SecureSocketConfig secureSocketConfig) async { + try { + Random r = Random(); + String key = base64.encode(List.generate(8, (_) => r.nextInt(256))); + + SecurityContext context = SecurityContext.defaultContext; + context.setAlpnProtocols(['http/1.1'], false); + HttpClient client = HttpClient(context: context); + + Uri uri = Uri.parse("https://$host:$port/ws"); + HttpClientRequest request = await client.getUrl(uri); + request.headers.add('Connection', 'upgrade'); + request.headers.add('Upgrade', 'websocket'); + request.headers.add( + 'sec-websocket-version', '13'); // insert the correct version here + request.headers.add('sec-websocket-key', key); + + HttpClientResponse response = await request.close(); + Socket socket = await response.detachSocket(); + + WebSocket ws = WebSocket.fromUpgradedSocket( + socket, + serverSide: false, + ); + + logger.finer('WebSocket connection established'); + + return ws; + } catch (e) { + throw AtException('Error creating WebSocket connection: ${e.toString()}'); + } + } + + /// Creates a secure socket connection (SecureSocket). + static Future _createSecureSocket( String host, String port, SecureSocketConfig secureSocketConfig) async { - SecureSocket? _secureSocket; + SecureSocket? secureSocket; if (!secureSocketConfig.decryptPackets) { - _secureSocket = await SecureSocket.connect(host, int.parse(port)); - _secureSocket.setOption(SocketOption.tcpNoDelay, true); - return _secureSocket; + secureSocket = await SecureSocket.connect(host, int.parse(port)); + secureSocket.setOption(SocketOption.tcpNoDelay, true); + return secureSocket; } else { SecurityContext securityContext = SecurityContext(); try { @@ -20,16 +72,17 @@ class SecureSocketUtil { .setTrustedCertificates(secureSocketConfig.pathToCerts!); } else { throw AtException( - 'decryptPackets set to true but path to trusted certificated not provided'); + 'decryptPackets set to true but path to trusted certificates not provided'); } - _secureSocket = await SecureSocket.connect(host, int.parse(port), + secureSocket = await SecureSocket.connect(host, int.parse(port), context: securityContext, keyLog: (line) => keysFile.writeAsStringSync(line, mode: FileMode.append)); - _secureSocket.setOption(SocketOption.tcpNoDelay, true); - return _secureSocket; + secureSocket.setOption(SocketOption.tcpNoDelay, true); + return secureSocket; } catch (e) { - throw AtException(e.toString()); + throw AtException( + 'Error creating SecureSocket connection: ${e.toString()}'); } } } diff --git a/packages/at_lookup/pubspec.yaml b/packages/at_lookup/pubspec.yaml index b0318179..24d90be9 100644 --- a/packages/at_lookup/pubspec.yaml +++ b/packages/at_lookup/pubspec.yaml @@ -1,12 +1,12 @@ name: at_lookup description: A Dart library that contains the core commands that can be used with a secondary server (scan, update, lookup, llookup, plookup, etc.) -version: 3.0.49 +version: 4.0.0 repository: https://github.com/atsign-foundation/at_libraries homepage: https://atsign.com documentation: https://docs.atsign.com/ environment: - sdk: '>=2.15.0 <4.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: path: ^1.8.0 diff --git a/packages/at_lookup/test/at_lookup_test.dart b/packages/at_lookup/test/at_lookup_test.dart index c248089f..462fdf63 100644 --- a/packages/at_lookup/test/at_lookup_test.dart +++ b/packages/at_lookup/test/at_lookup_test.dart @@ -6,10 +6,10 @@ import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; import 'package:at_lookup/src/connection/at_connection.dart'; -import 'package:at_lookup/src/connection/outbound_message_listener.dart'; -import 'package:test/test.dart'; -import 'package:mocktail/mocktail.dart'; +import 'package:at_lookup/src/connection/at_message_listener.dart'; import 'package:at_utils/at_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; import 'at_lookup_test_utils.dart'; @@ -17,441 +17,731 @@ class FakeAtSigningInput extends Fake implements AtSigningInput {} void main() { AtSignLogger.root_level = 'finest'; - late OutboundConnection mockOutBoundConnection; + late AtConnection mockAtConnection; + late SecondaryAddressFinder mockSecondaryAddressFinder; - late OutboundMessageListener mockOutboundListener; - late AtLookupSecureSocketFactory mockSocketFactory; - late AtLookupSecureSocketListenerFactory mockSecureSocketListenerFactory; - late AtLookupOutboundConnectionFactory mockOutboundConnectionFactory; + late AtMessageListener mockAtMessageListener; + late AtLookupConnectionFactory mockAtConnectionFactory; late AtChops mockAtChops; late SecureSocket mockSecureSocket; + late WebSocket mockWebSocket; String atServerHost = '127.0.0.1'; int atServerPort = 12345; - setUp(() { - mockOutBoundConnection = MockOutboundConnectionImpl(); - mockSecondaryAddressFinder = MockSecondaryAddressFinder(); - mockOutboundListener = MockOutboundMessageListener(); - mockSocketFactory = MockSecureSocketFactory(); - mockSecureSocketListenerFactory = MockSecureSocketListenerFactory(); - mockOutboundConnectionFactory = MockOutboundConnectionFactory(); - mockAtChops = MockAtChops(); - registerFallbackValue(SecureSocketConfig()); - mockSecureSocket = createMockAtServerSocket(atServerHost, atServerPort); - - when(() => mockSecondaryAddressFinder.findSecondary('@alice')) - .thenAnswer((_) async { - return SecondaryAddress(atServerHost, atServerPort); - }); - when(() => mockSocketFactory.createSocket(atServerHost, '12345', any())) - .thenAnswer((invocation) { - return Future.value(mockSecureSocket); - }); - when(() => mockOutboundConnectionFactory - .createOutboundConnection(mockSecureSocket)).thenAnswer((invocation) { - print('Creating mock outbound connection'); - return mockOutBoundConnection; - }); - when(() => mockSecureSocketListenerFactory - .createListener(mockOutBoundConnection)).thenAnswer((invocation) { - print('creating mock outbound listener'); - return mockOutboundListener; - }); - when(() => mockOutBoundConnection.write('from:@alice\n')) - .thenAnswer((invocation) { - mockSecureSocket.write('from:@alice\n'); - return Future.value(); - }); - }); + group('A group of secure socket tests', () { + setUp(() { + mockAtConnection = MockAtSocketConnection(); + mockSecondaryAddressFinder = MockSecondaryAddressFinder(); + mockAtMessageListener = MockAtMessageListener(); + mockAtConnectionFactory = MockAtLookupConnectionFactory(); + mockAtChops = MockAtChops(); - group('A group of tests to verify atlookup pkam authentication', () { - test('pkam auth without enrollmentId - auth success', () async { - final pkamSignature = - 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; - - AtSigningResult mockSigningResult = AtSigningResult() - ..result = 'mock_signing_result'; - registerFallbackValue(FakeAtSigningInput()); - when(() => mockAtChops.sign(any())).thenAnswer((_) => mockSigningResult); - - when(() => mockAtChops.sign(any())) - .thenReturn(AtSigningResult()..result = pkamSignature); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value('data:success')); - - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(OutboundConnectionMetadata()..isAuthenticated = false); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - when(() => mockOutBoundConnection.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) - .thenAnswer((invocation) { - mockSecureSocket.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); - return Future.value(); + registerFallbackValue(SecureSocketConfig()); + mockSecureSocket = createMockAtServerSocket(atServerHost, atServerPort); + + when(() => mockSecondaryAddressFinder.findSecondary('@alice')) + .thenAnswer((_) async { + return SecondaryAddress(atServerHost, atServerPort); + }); + + when(() => mockAtConnectionFactory.createUnderlying( + atServerHost, '12345', any())).thenAnswer((_) { + return Future.value(mockSecureSocket); }); - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - var result = await atLookup.pkamAuthenticate(); - expect(result, true); + when(() => mockAtConnectionFactory.createConnection(mockSecureSocket)) + .thenAnswer((_) => mockAtConnection); + + when(() => mockAtConnection.write(any())) + .thenAnswer((_) => Future.value()); + + when(() => mockAtConnectionFactory.createListener(mockAtConnection)) + .thenAnswer((_) => mockAtMessageListener); }); - test('pkam auth without enrollmentId - auth failed', () async { - final pkamSignature = - 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; - - AtSigningResult mockSigningResult = AtSigningResult() - ..result = 'mock_signing_result'; - registerFallbackValue(FakeAtSigningInput()); - when(() => mockAtChops.sign(any())).thenAnswer((_) => mockSigningResult); - - when(() => mockAtChops.sign(any())) - .thenReturn(AtSigningResult()..result = pkamSignature); - when(() => mockOutboundListener.read()).thenAnswer((_) => - Future.value('error:AT0401-Exception: pkam authentication failed')); - - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(OutboundConnectionMetadata()..isAuthenticated = false); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - when(() => mockOutBoundConnection.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) - .thenAnswer((invocation) { - mockSecureSocket.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); - return Future.value(); + group('A group of tests to verify atlookup pkam authentication', () { + test('pkam auth without enrollmentId - auth success', () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value('data:success')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) + .thenAnswer((invocation) { + mockSecureSocket.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + // Override atConnectionFactory with mock in AtLookupImpl + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + var result = await atLookup.pkamAuthenticate(); + expect(result, true); }); - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - expect(() async => await atLookup.pkamAuthenticate(), - throwsA(predicate((e) => e is UnAuthenticatedException))); - }); + test('pkam auth without enrollmentId - auth failed', () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()).thenAnswer((_) => + Future.value('error:AT0401-Exception: pkam authentication failed')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) + .thenAnswer((invocation) { + mockSecureSocket.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + // Override atConnectionFactory with mock in AtLookupImpl + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + expect(() async => await atLookup.pkamAuthenticate(), + throwsA(predicate((e) => e is UnAuthenticatedException))); + }); - test('pkam auth with enrollmentId - auth success', () async { - final pkamSignature = - 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; - final enrollmentIdFromServer = '5a21feb4-dc04-4603-829c-15f523789170'; - AtSigningResult mockSigningResult = AtSigningResult() - ..result = 'mock_signing_result'; - registerFallbackValue(FakeAtSigningInput()); - when(() => mockAtChops.sign(any())).thenAnswer((_) => mockSigningResult); - - when(() => mockAtChops.sign(any())) - .thenReturn(AtSigningResult()..result = pkamSignature); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value('data:success')); - - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(OutboundConnectionMetadata()..isAuthenticated = false); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - when(() => mockOutBoundConnection.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n')) - .thenAnswer((invocation) { - mockSecureSocket.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n'); - return Future.value(); + test('pkam auth with enrollmentId - auth success', () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + final enrollmentIdFromServer = '5a21feb4-dc04-4603-829c-15f523789170'; + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value('data:success')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n')) + .thenAnswer((invocation) { + mockSecureSocket.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + var result = await atLookup.pkamAuthenticate( + enrollmentId: enrollmentIdFromServer); + expect(result, true); }); - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - var result = - await atLookup.pkamAuthenticate(enrollmentId: enrollmentIdFromServer); - expect(result, true); + test('pkam auth with enrollmentId - auth failed', () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + final enrollmentIdFromServer = '5a21feb4-dc04-4603-829c-15f523789170'; + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()).thenAnswer((_) => + Future.value('error:AT0401-Exception: pkam authentication failed')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n')) + .thenAnswer((invocation) { + mockSecureSocket.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + expect( + () async => await atLookup.pkamAuthenticate( + enrollmentId: enrollmentIdFromServer), + throwsA(predicate((e) => + e is UnAuthenticatedException && + e.message.contains('AT0401')))); + }); }); - test('pkam auth with enrollmentId - auth failed', () async { - final pkamSignature = - 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; - final enrollmentIdFromServer = '5a21feb4-dc04-4603-829c-15f523789170'; - AtSigningResult mockSigningResult = AtSigningResult() - ..result = 'mock_signing_result'; - registerFallbackValue(FakeAtSigningInput()); - when(() => mockAtChops.sign(any())).thenAnswer((_) => mockSigningResult); - - when(() => mockAtChops.sign(any())) - .thenReturn(AtSigningResult()..result = pkamSignature); - when(() => mockOutboundListener.read()).thenAnswer((_) => - Future.value('error:AT0401-Exception: pkam authentication failed')); - - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(OutboundConnectionMetadata()..isAuthenticated = false); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - when(() => mockOutBoundConnection.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n')) - .thenAnswer((invocation) { - mockSecureSocket.write( - 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n'); - return Future.value(); + group('A group of tests to verify executeCommand method', () { + test('executeCommand - from verb - auth false', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + final fromResponse = + 'data:_03fe0ff2-ac50-4c80-8f43-88480beba888@alice:c3d345fc-5691-4f90-bc34-17cba31f060f'; + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(fromResponse)); + var result = await atLookup.executeCommand('from:@alice\n'); + expect(result, fromResponse); + }, timeout: Timeout(Duration(minutes: 5))); + + test('executeCommand -llookup verb - auth true - auth key not set', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + final fromResponse = 'data:1234'; + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(fromResponse)); + expect( + () async => await atLookup.executeCommand('llookup:phone@alice\n', + auth: true), + throwsA(predicate((e) => e is UnAuthenticatedException))); }); - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - expect( - () async => await atLookup.pkamAuthenticate( - enrollmentId: enrollmentIdFromServer), - throwsA(predicate((e) => - e is UnAuthenticatedException && e.message.contains('AT0401')))); - }); - }); + test('executeCommand -llookup verb - auth true - at_chops set', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + final llookupCommand = 'llookup:phone@alice\n'; + final llookupResponse = 'data:1234'; + when(() => mockAtConnection.write(llookupCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(llookupCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(llookupResponse)); + var result = await atLookup.executeCommand(llookupCommand); + expect(result, llookupResponse); + }); - group('A group of tests to verify executeCommand method', () { - test('executeCommand - from verb - auth false', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - final fromResponse = - 'data:_03fe0ff2-ac50-4c80-8f43-88480beba888@alice:c3d345fc-5691-4f90-bc34-17cba31f060f'; - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(fromResponse)); - var result = await atLookup.executeCommand('from:@alice\n'); - expect(result, fromResponse); - }); + test('executeCommand - test non json error handling', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + final llookupCommand = 'llookup:phone@alice\n'; + final llookupResponse = 'error:AT0015-Exception: fubar'; + when(() => mockAtConnection.write(llookupCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(llookupCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(llookupResponse)); + await expectLater( + atLookup.executeCommand(llookupCommand), + throwsA(predicate((e) => + e is AtLookUpException && + e.errorMessage == 'Exception: fubar'))); + }); - test('executeCommand -llookup verb - auth true - auth key not set', - () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - final fromResponse = 'data:1234'; - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(fromResponse)); - expect( - () async => await atLookup.executeCommand('llookup:phone@alice\n', - auth: true), - throwsA(predicate((e) => e is UnAuthenticatedException))); + test('executeCommand - test json error handling', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + final llookupCommand = 'llookup:phone@alice\n'; + final llookupResponse = + 'error:{"errorCode":"AT0015","errorDescription":"Exception: fubar"}'; + when(() => mockAtConnection.write(llookupCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(llookupCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(llookupResponse)); + await expectLater( + atLookup.executeCommand(llookupCommand), + throwsA(predicate((e) => + e is AtLookUpException && + e.errorMessage == 'Exception: fubar'))); + }); }); - test('executeCommand -llookup verb - auth true - at_chops set', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - final llookupCommand = 'llookup:phone@alice\n'; - final llookupResponse = 'data:1234'; - when(() => mockOutBoundConnection.write(llookupCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(llookupCommand); - return Future.value(); + group('Validate executeVerb() behaviour', () { + test('validate EnrollVerbHandler behaviour - request', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + String appName = 'unit_test_1'; + String deviceName = 'test_device'; + String otp = 'ABCDEF'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.request + ..appName = appName + ..deviceName = deviceName + ..otp = otp; + String enrollCommand = + 'enroll:request:{"appName":"$appName","deviceName":"$deviceName","otp":"$otp"}\n'; + final enrollResponse = + 'data:{"enrollmentId":"1234567890","status":"pending"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = false; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + var result = await atLookup.executeVerb(enrollVerbBuilder); + expect(result, enrollResponse); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(llookupResponse)); - var result = await atLookup.executeCommand(llookupCommand); - expect(result, llookupResponse); - }); - test('executeCommand - test non json error handling', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - final llookupCommand = 'llookup:phone@alice\n'; - final llookupResponse = 'error:AT0015-Exception: fubar'; - when(() => mockOutBoundConnection.write(llookupCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(llookupCommand); - return Future.value(); + test('validate behaviour with EnrollVerbHandler - approve', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atChops = mockAtChops; + atLookup.atConnectionFactory = mockAtConnectionFactory; + String appName = 'unit_test_2'; + String deviceName = 'test_device'; + String enrollmentId = '1357913579'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.approve + ..enrollmentId = '1357913579' + ..appName = appName + ..deviceName = deviceName; + String enrollCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId","appName":"$appName","deviceName":"$deviceName"}\n'; + final enrollResponse = + 'data:{"enrollmentId":"1357913579","status":"approved"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(llookupResponse)); - await expectLater( - atLookup.executeCommand(llookupCommand), - throwsA(predicate((e) => - e is AtLookUpException && e.errorMessage == 'Exception: fubar'))); - }); - test('executeCommand - test json error handling', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - final llookupCommand = 'llookup:phone@alice\n'; - final llookupResponse = - 'error:{"errorCode":"AT0015","errorDescription":"Exception: fubar"}'; - when(() => mockOutBoundConnection.write(llookupCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(llookupCommand); - return Future.value(); + test('validate behaviour with EnrollVerbHandler - revoke', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atChops = mockAtChops; + atLookup.atConnectionFactory = mockAtConnectionFactory; + String enrollmentId = '89213647826348'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.revoke + ..enrollmentId = enrollmentId; + String enrollCommand = + 'enroll:revoke:{"enrollmentId":"$enrollmentId"}\n'; + String enrollResponse = + 'data:{"enrollmentId":"$enrollmentId","status":"revoked"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); + }); + + test('validate behaviour with EnrollVerbHandler - deny', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + String enrollmentId = '5754765754'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.deny + ..enrollmentId = enrollmentId; + String enrollCommand = 'enroll:deny:{"enrollmentId":"$enrollmentId"}\n'; + String enrollResponse = + 'data:{"enrollmentId":"$enrollmentId","status":"denied"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockSecureSocket.write(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(llookupResponse)); - await expectLater( - atLookup.executeCommand(llookupCommand), - throwsA(predicate((e) => - e is AtLookUpException && e.errorMessage == 'Exception: fubar'))); }); }); - group('Validate executeVerb() behaviour', () { - test('validate EnrollVerbHandler behaviour - request', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - - String appName = 'unit_test_1'; - String deviceName = 'test_device'; - String otp = 'ABCDEF'; - - EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() - ..operation = EnrollOperationEnum.request - ..appName = appName - ..deviceName = deviceName - ..otp = otp; - String enrollCommand = - 'enroll:request:{"appName":"$appName","deviceName":"$deviceName","otp":"$otp"}\n'; - final enrollResponse = - 'data:{"enrollmentId":"1234567890","status":"pending"}'; - - when(() => mockOutBoundConnection.write(enrollCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(enrollCommand); - return Future.value(); + group('A group of web socket tests', () { + setUp(() { + mockAtConnection = MockAtSocketConnection(); + mockSecondaryAddressFinder = MockSecondaryAddressFinder(); + mockAtMessageListener = MockAtMessageListener(); + mockAtConnectionFactory = MockAtLookupConnectionFactory(); + mockAtChops = MockAtChops(); + registerFallbackValue(SecureSocketConfig()); + mockWebSocket = createMockWebSocket(atServerHost, atServerPort); + + when(() => mockSecondaryAddressFinder.findSecondary('@alice')) + .thenAnswer((_) async { + return SecondaryAddress(atServerHost, atServerPort); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(enrollResponse)); - AtConnectionMetaData? atConnectionMetaData = OutboundConnectionMetadata() - ..isAuthenticated = false; - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(atConnectionMetaData); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - var result = await atLookup.executeVerb(enrollVerbBuilder); - expect(result, enrollResponse); + + when(() => mockAtConnectionFactory.createUnderlying( + atServerHost, '12345', any())).thenAnswer((_) { + return Future.value(mockWebSocket); + }); + + when(() => mockAtConnectionFactory.createConnection(mockWebSocket)) + .thenAnswer((_) => mockAtConnection); + + when(() => mockAtConnection.write(any())) + .thenAnswer((_) => Future.value()); + + when(() => mockAtConnectionFactory.createListener(mockAtConnection)) + .thenAnswer((_) => mockAtMessageListener); }); - test('validate behaviour with EnrollVerbHandler - approve', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - - String appName = 'unit_test_2'; - String deviceName = 'test_device'; - String enrollmentId = '1357913579'; - - EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() - ..operation = EnrollOperationEnum.approve - ..enrollmentId = '1357913579' - ..appName = appName - ..deviceName = deviceName; - String enrollCommand = - 'enroll:approve:{"enrollmentId":"$enrollmentId","appName":"$appName","deviceName":"$deviceName"}\n'; - final enrollResponse = - 'data:{"enrollmentId":"1357913579","status":"approved"}'; - - when(() => mockOutBoundConnection.write(enrollCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(enrollCommand); - return Future.value(); + group('A group of tests to verify atlookup pkam authentication', () { + test('pkam auth using websocket without enrollmentId - auth success', + () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value('data:success')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) + .thenAnswer((invocation) { + mockWebSocket.add( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + // Override atConnectionFactory with mock in AtLookupImpl + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + var result = await atLookup.pkamAuthenticate(); + expect(result, true); + }, timeout: Timeout(Duration(minutes: 2))); + + test('pkam auth using a websocket without enrollmentId - auth failed', + () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()).thenAnswer((_) => + Future.value('error:AT0401-Exception: pkam authentication failed')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n')) + .thenAnswer((invocation) { + mockWebSocket.add( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + // Override atConnectionFactory with mock in AtLookupImpl + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + expect(() async => await atLookup.pkamAuthenticate(), + throwsA(predicate((e) => e is UnAuthenticatedException))); + }); + + test('pkam auth using a websocket with enrollmentId - auth success', + () async { + final pkamSignature = + 'MbNbIwCSxsHxm4CHyakSE2yLqjjtnmzpSLPcGG7h+4M/GQAiJkklQfd/x9z58CSJfuSW8baIms26SrnmuYePZURfp5oCqtwRpvt+l07Gnz8aYpXH0k5qBkSR34SBk4nb+hdAjsXXgfWWC56gROPMwpOEbuDS6esU7oku+a7Rdr10xrFlk1Tf2eRwPOMWyuKwOvLwSgyq/INAFRYav5RmLFiecQhPME6ssc1jW92wztylKBtuZT4rk8787b6Z9StxT4dPZzWjfV1+oYDLaqu2PcQS2ZthH+Wj8NgoogDxSP+R7BE1FOVJKnavpuQWeOqNWeUbKkSVP0B0DN6WopAdsg=='; + final enrollmentIdFromServer = '5a21feb4-dc04-4603-829c-15f523789170'; + AtSigningResult mockSigningResult = AtSigningResult() + ..result = 'mock_signing_result'; + registerFallbackValue(FakeAtSigningInput()); + when(() => mockAtChops.sign(any())) + .thenAnswer((_) => mockSigningResult); + + when(() => mockAtChops.sign(any())) + .thenReturn(AtSigningResult()..result = pkamSignature); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value('data:success')); + + when(() => mockAtConnection.metaData) + .thenReturn(AtConnectionMetaData()..isAuthenticated = false); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + when(() => mockAtConnection.write( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n')) + .thenAnswer((invocation) { + mockWebSocket.add( + 'pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:$enrollmentIdFromServer:$pkamSignature\n'); + return Future.value(); + }); + + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + var result = await atLookup.pkamAuthenticate( + enrollmentId: enrollmentIdFromServer); + expect(result, true); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(enrollResponse)); - AtConnectionMetaData? atConnectionMetaData = OutboundConnectionMetadata() - ..isAuthenticated = true; - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(atConnectionMetaData); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); - test('validate behaviour with EnrollVerbHandler - revoke', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - String enrollmentId = '89213647826348'; - - EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() - ..operation = EnrollOperationEnum.revoke - ..enrollmentId = enrollmentId; - String enrollCommand = 'enroll:revoke:{"enrollmentId":"$enrollmentId"}\n'; - String enrollResponse = - 'data:{"enrollmentId":"$enrollmentId","status":"revoked"}'; - - when(() => mockOutBoundConnection.write(enrollCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(enrollCommand); - return Future.value(); + group('A group of tests to verify executeCommand method', () { + test('executeCommand using websocket- from verb - auth false', () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atConnectionFactory = mockAtConnectionFactory; + final fromResponse = + 'data:_03fe0ff2-ac50-4c80-8f43-88480beba888@alice:c3d345fc-5691-4f90-bc34-17cba31f060f'; + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(fromResponse)); + var result = await atLookup.executeCommand('from:@alice\n'); + expect(result, fromResponse); + }, timeout: Timeout(Duration(minutes: 5))); + + test( + 'executeCommand using websocket-llookup verb - auth true - auth key not set', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + final fromResponse = 'data:1234'; + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(fromResponse)); + expect( + () async => await atLookup.executeCommand('llookup:phone@alice\n', + auth: true), + throwsA(predicate((e) => e is UnAuthenticatedException))); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(enrollResponse)); - AtConnectionMetaData? atConnectionMetaData = OutboundConnectionMetadata() - ..isAuthenticated = true; - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(atConnectionMetaData); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); - test('validate behaviour with EnrollVerbHandler - deny', () async { - final atLookup = AtLookupImpl('@alice', atServerHost, 64, - secondaryAddressFinder: mockSecondaryAddressFinder, - secureSocketFactory: mockSocketFactory, - socketListenerFactory: mockSecureSocketListenerFactory, - outboundConnectionFactory: mockOutboundConnectionFactory); - atLookup.atChops = mockAtChops; - String enrollmentId = '5754765754'; - - EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() - ..operation = EnrollOperationEnum.deny - ..enrollmentId = enrollmentId; - String enrollCommand = 'enroll:deny:{"enrollmentId":"$enrollmentId"}\n'; - String enrollResponse = - 'data:{"enrollmentId":"$enrollmentId","status":"denied"}'; - - when(() => mockOutBoundConnection.write(enrollCommand)) - .thenAnswer((invocation) { - mockSecureSocket.write(enrollCommand); - return Future.value(); + group('Validate executeVerb() behaviour', () { + test( + 'validate EnrollVerbHandler behaviour using a websocket connection- request', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atConnectionFactory = mockAtConnectionFactory; + String appName = 'unit_test_1'; + String deviceName = 'test_device'; + String otp = 'ABCDEF'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.request + ..appName = appName + ..deviceName = deviceName + ..otp = otp; + String enrollCommand = + 'enroll:request:{"appName":"$appName","deviceName":"$deviceName","otp":"$otp"}\n'; + final enrollResponse = + 'data:{"enrollmentId":"1234567890","status":"pending"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockWebSocket.add(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = false; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + var result = await atLookup.executeVerb(enrollVerbBuilder); + expect(result, enrollResponse); + }); + + test( + 'validate behaviour with EnrollVerbHandler using a websocket connection- approve', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atChops = mockAtChops; + atLookup.atConnectionFactory = mockAtConnectionFactory; + String appName = 'unit_test_2'; + String deviceName = 'test_device'; + String enrollmentId = '1357913579'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.approve + ..enrollmentId = '1357913579' + ..appName = appName + ..deviceName = deviceName; + String enrollCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId","appName":"$appName","deviceName":"$deviceName"}\n'; + final enrollResponse = + 'data:{"enrollmentId":"1357913579","status":"approved"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockWebSocket.add(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); + }); + + test( + 'validate behaviour with EnrollVerbHandler using a websocket connection - revoke', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atChops = mockAtChops; + atLookup.atConnectionFactory = mockAtConnectionFactory; + String enrollmentId = '89213647826348'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.revoke + ..enrollmentId = enrollmentId; + String enrollCommand = + 'enroll:revoke:{"enrollmentId":"$enrollmentId"}\n'; + String enrollResponse = + 'data:{"enrollmentId":"$enrollmentId","status":"revoked"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockWebSocket.add(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); + }); + + test( + 'validate behaviour with EnrollVerbHandler using a websocket connection - deny', + () async { + final atLookup = AtLookupImpl('@alice', atServerHost, 64, + secondaryAddressFinder: mockSecondaryAddressFinder, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atConnectionFactory = mockAtConnectionFactory; + atLookup.atChops = mockAtChops; + String enrollmentId = '5754765754'; + + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.deny + ..enrollmentId = enrollmentId; + String enrollCommand = 'enroll:deny:{"enrollmentId":"$enrollmentId"}\n'; + String enrollResponse = + 'data:{"enrollmentId":"$enrollmentId","status":"denied"}'; + + when(() => mockAtConnection.write(enrollCommand)) + .thenAnswer((invocation) { + mockWebSocket.add(enrollCommand); + return Future.value(); + }); + when(() => mockAtMessageListener.read()) + .thenAnswer((_) => Future.value(enrollResponse)); + AtConnectionMetaData? atConnectionMetaData = AtConnectionMetaData() + ..isAuthenticated = true; + when(() => mockAtConnection.metaData).thenReturn(atConnectionMetaData); + when(() => mockAtConnection.isInValid()).thenReturn(false); + + expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); - when(() => mockOutboundListener.read()) - .thenAnswer((_) => Future.value(enrollResponse)); - AtConnectionMetaData? atConnectionMetaData = OutboundConnectionMetadata() - ..isAuthenticated = true; - when(() => mockOutBoundConnection.getMetaData()) - .thenReturn(atConnectionMetaData); - when(() => mockOutBoundConnection.isInValid()).thenReturn(false); - - expect(await atLookup.executeVerb(enrollVerbBuilder), enrollResponse); }); }); } diff --git a/packages/at_lookup/test/at_lookup_test_utils.dart b/packages/at_lookup/test/at_lookup_test_utils.dart index 16f16287..43474842 100644 --- a/packages/at_lookup/test/at_lookup_test_utils.dart +++ b/packages/at_lookup/test/at_lookup_test_utils.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'package:at_chops/at_chops.dart'; import 'package:at_lookup/at_lookup.dart'; -import 'package:at_lookup/src/connection/outbound_message_listener.dart'; +import 'package:at_lookup/src/connection/at_message_listener.dart'; +import 'package:at_lookup/src/connection/at_socket_connection.dart'; import 'package:mocktail/mocktail.dart'; int mockSocketNumber = 1; @@ -13,8 +14,8 @@ class MockSecondaryAddressFinder extends Mock class MockSecondaryUrlFinder extends Mock implements SecondaryUrlFinder {} -class MockSecureSocketFactory extends Mock - implements AtLookupSecureSocketFactory {} +class MockAtLookupConnectionFactory extends Mock + implements AtLookupConnectionFactory {} class MockStreamSubscription extends Mock implements StreamSubscription {} @@ -23,19 +24,16 @@ class MockSecureSocket extends Mock implements SecureSocket { int mockNumber = mockSocketNumber++; } -class MockSecureSocketListenerFactory extends Mock - implements AtLookupSecureSocketListenerFactory {} - -class MockOutboundConnectionFactory extends Mock - implements AtLookupOutboundConnectionFactory {} +class MockWebSocket extends Mock implements WebSocket { + bool destroyed = false; + int mockNumber = mockSocketNumber++; +} -class MockOutboundMessageListener extends Mock - implements OutboundMessageListener {} +class MockAtMessageListener extends Mock implements AtMessageListener {} class MockAtChops extends Mock implements AtChopsImpl {} -class MockOutboundConnectionImpl extends Mock - implements OutboundConnectionImpl {} +class MockAtSocketConnection extends Mock implements AtSocketConnection {} SecureSocket createMockAtServerSocket(String address, int port) { SecureSocket mss = MockSecureSocket(); @@ -50,3 +48,17 @@ SecureSocket createMockAtServerSocket(String address, int port) { onDone: any(named: "onDone"))).thenReturn(MockStreamSubscription()); return mss; } + +WebSocket createMockWebSocket(String address, int port) { + var mockWebSocket = MockWebSocket(); + when(() => mockWebSocket.close(any(), any())).thenAnswer((_) async { + (mockWebSocket).destroyed = true; + }); + when(() => mockWebSocket.add(any())).thenReturn(null); + when(() => mockWebSocket.listen(any(), + onError: any(named: "onError"), + onDone: any(named: "onDone"), + cancelOnError: any(named: "cancelOnError"))) + .thenReturn(MockStreamSubscription()); + return mockWebSocket; +} diff --git a/packages/at_lookup/test/connection_management_test.dart b/packages/at_lookup/test/connection_management_test.dart index 46de1a5e..49493c4f 100644 --- a/packages/at_lookup/test/connection_management_test.dart +++ b/packages/at_lookup/test/connection_management_test.dart @@ -3,35 +3,49 @@ import 'dart:io'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; -import 'package:at_lookup/src/connection/outbound_message_listener.dart'; -import 'package:test/test.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_lookup/src/connection/at_message_listener.dart'; +import 'package:at_lookup/src/connection/at_socket_connection.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; import 'at_lookup_test_utils.dart'; -class MockOutboundConnectionImpl extends Mock - implements OutboundConnectionImpl {} +class MockAtLookupSecureSocketFactory extends Mock + implements AtLookupSecureSocketFactory {} void main() { group('test connection close and socket cleanup', () { late SecondaryAddressFinder finder; - late MockSecureSocketFactory mockSocketFactory; + late MockAtLookupConnectionFactory mockAtConnectionFactory; + late AtMessageListener mockAtMessageListener; setUp(() { - mockSocketNumber = 1; - + mockSocketNumber = 1; // Reset counter for each test finder = MockSecondaryAddressFinder(); - when(() => finder.findSecondary(any())).thenAnswer((invocation) => - Future.value( - SecondaryAddress('test.test.test', 12345))); + when(() => finder.findSecondary(any())) + .thenAnswer((_) async => SecondaryAddress('test.test.test', 12345)); - mockSocketFactory = MockSecureSocketFactory(); + mockAtConnectionFactory = MockAtLookupConnectionFactory(); registerFallbackValue(SecureSocketConfig()); - when(() => - mockSocketFactory.createSocket('test.test.test', '12345', any())) - .thenAnswer((invocation) { - return Future.value( - createMockAtServerSocket('test.test.test', 12345)); + + mockAtMessageListener = MockAtMessageListener(); + + when(() => mockAtConnectionFactory.createUnderlying( + 'test.test.test', '12345', any())).thenAnswer((_) { + // Each call provides a new instance of MockSecureSocket + SecureSocket newMockSocket = + createMockAtServerSocket('test.test.test', 12345); + AtSocketConnection atConnection = AtSocketConnection(newMockSocket); + + // Update factory to return this new connection and listener + when(() => + mockAtConnectionFactory.createConnection(newMockSocket)) + .thenReturn(atConnection); + + when(() => mockAtConnectionFactory.createListener(atConnection)) + .thenAnswer((_) => mockAtMessageListener); + return Future.value(newMockSocket); }); }); @@ -39,33 +53,87 @@ void main() { 'test AtLookupImpl will use its default SecureSocketFactory if none is provided to it', () async { AtLookupImpl atLookup = AtLookupImpl('@alice', 'test.test.test', 64, - secondaryAddressFinder: finder, secureSocketFactory: null); + secondaryAddressFinder: finder); - expect(atLookup.socketFactory.runtimeType.toString(), + expect(atLookup.atConnectionFactory.runtimeType.toString(), "AtLookupSecureSocketFactory"); expect(() async => await atLookup.createConnection(), throwsA(predicate((dynamic e) => e is SecondaryConnectException))); }); + test( + 'A test to verify the connections are invalidated after the connection time-outs', + () async { + String host = 'test.host'; + int port = 64; + + SecureSocket mockSecureSocket = MockSecureSocket(); + MockAtLookupSecureSocketFactory mockAtLookupSecureSocketFactory = + MockAtLookupSecureSocketFactory(); + SecondaryAddressFinder mockSecondaryAddressFinder = + MockSecondaryAddressFinder(); + + AtLookupImpl atLookup = AtLookupImpl('@alice', host, port, + secondaryAddressFinder: mockSecondaryAddressFinder); + SecureSocketConfig secureSocketConfig = SecureSocketConfig(); + + // Setting mock instances + atLookup.atConnectionFactory = mockAtLookupSecureSocketFactory; + + // Set mock responses. + when(() => mockAtLookupSecureSocketFactory.createUnderlying( + host, '$port', secureSocketConfig)) + .thenAnswer((_) => Future.value(mockSecureSocket)); + + when(() => mockSecureSocket.setOption(SocketOption.tcpNoDelay, true)) + .thenReturn(true); + + // In the constructor of [AtSocketConnection] which is super class of AtConnection + // socket.setOption is invoked. Therefore, the initialization of AtSocketConnection + // should be executed after when(() => mockSecureSocket.setOption). + // Otherwise a null exception arises. + AtSocketConnection atConnection = AtSocketConnection(mockSecureSocket); + + when(() => mockAtLookupSecureSocketFactory + .createConnection(mockSecureSocket)).thenReturn(atConnection); + + when(() => mockAtLookupSecureSocketFactory.createListener(atConnection)) + .thenReturn(AtMessageListener(atConnection)); + + // Setting connection timeout to 2 seconds. + atLookup.atConnectionTimeout = Duration(seconds: 2).inMilliseconds; + // Create connection. + bool isConnCreated = + await atLookup.createAtConnection(host, '$port', secureSocketConfig); + expect(isConnCreated, true); + expect(atLookup.connection?.isInValid(), false); + // Wait for the connection to timeout. + await Future.delayed(Duration(seconds: 2)); + expect(atLookup.connection?.isInValid(), true); + }); + test( 'test AtLookupImpl closes invalid connections before creating new ones', () async { AtLookupImpl atLookup = AtLookupImpl('@alice', 'test.test.test', 64, - secondaryAddressFinder: finder, - secureSocketFactory: mockSocketFactory); - expect(atLookup.socketFactory.runtimeType.toString(), - "MockSecureSocketFactory"); + secondaryAddressFinder: finder); + expect(atLookup.atConnectionFactory.runtimeType.toString(), + "AtLookupSecureSocketFactory"); + + // Override atConnectionFactory with mock in AtLookupImpl + atLookup.atConnectionFactory = mockAtConnectionFactory; await atLookup.createConnection(); // let's get a handle to the first socket & connection - OutboundConnection firstConnection = atLookup.connection!; + AtSocketConnection firstConnection = + atLookup.connection as AtSocketConnection; // Explicit cast MockSecureSocket firstSocket = - firstConnection.getSocket() as MockSecureSocket; + firstConnection.underlying as MockSecureSocket; expect(firstSocket.mockNumber, 1); expect(firstSocket.destroyed, false); - expect(firstConnection.metaData!.isClosed, false); + expect(firstConnection.metaData.isClosed, false); expect(firstConnection.isInValid(), false); // Make the connection appear 'idle' @@ -73,6 +141,8 @@ void main() { await Future.delayed(Duration(milliseconds: 2)); expect(firstConnection.isInValid(), true); + atLookup.atConnectionFactory = mockAtConnectionFactory; + // When we now call AtLookupImpl's createConnection again, it should: // - notice that its current connection is 'idle', and close it // - create a new connection @@ -80,71 +150,72 @@ void main() { // has the first connection been closed, and its socket destroyed? expect(firstSocket.destroyed, true); - expect(firstConnection.metaData!.isClosed, true); + expect(firstConnection.metaData.isClosed, true); // has a new connection been created, with a new socket? - OutboundConnection secondConnection = atLookup.connection!; + AtSocketConnection secondConnection = + atLookup.connection as AtSocketConnection; // Explicit cast MockSecureSocket secondSocket = - secondConnection.getSocket() as MockSecureSocket; + secondConnection.underlying as MockSecureSocket; expect(firstConnection.hashCode == secondConnection.hashCode, false); expect(secondSocket.mockNumber, 2); expect(secondSocket.destroyed, false); - expect(secondConnection.metaData!.isClosed, false); + expect(secondConnection.metaData.isClosed, false); expect(secondConnection.isInValid(), false); }); test( 'test message listener closes connection' ' when socket listener onDone is called', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, false); + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, false); oml.onSocketDone(); - expect((oc.getSocket() as MockSecureSocket).destroyed, true); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, true); + expect(oc.metaData.isClosed, true); }); test( 'test message listener closes connection' ' when socket listener onError is called', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, false); + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, false); oml.onSocketError('test'); - expect((oc.getSocket() as MockSecureSocket).destroyed, true); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, true); + expect(oc.metaData.isClosed, true); }); test('test can safely call connection.close() repeatedly', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, false); + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, false); await oml.closeConnection(); - expect((oc.getSocket() as MockSecureSocket).destroyed, true); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, true); + expect(oc.metaData.isClosed, true); - (oc.getSocket() as MockSecureSocket).destroyed = false; + (oc.underlying as MockSecureSocket).destroyed = false; await oml.closeConnection(); // Since the connection was already closed above, // we don't expect destroy to be called on the socket again - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, true); }); test( - 'test that OutboundMessageListener.closeConnection will call' + 'test that AtMessageListener.closeConnection will call' ' connection.close if the connection is idle', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, false); + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, false); expect(oc.isInValid(), false); // Make the connection appear 'idle' @@ -154,39 +225,39 @@ void main() { await oml.closeConnection(); - expect((oc.getSocket() as MockSecureSocket).destroyed, true); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, true); + expect(oc.metaData.isClosed, true); }); test( - 'test that OutboundMessageListener.closeConnection will not call' + 'test that AtMessageListener.closeConnection will not call' ' connection.close if already marked closed', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - oc.metaData!.isClosed = true; + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + oc.metaData.isClosed = true; await oml.closeConnection(); // socketDestroyed will be set in these tests only if socket.destroy() is called - expect((oc.getSocket() as MockSecureSocket).destroyed, false); + expect((oc.underlying as MockSecureSocket).destroyed, false); }); test( - 'test that OutboundMessageListener.closeConnection will call' + 'test that AtMessageListener.closeConnection will call' ' connection.close even if the connection is marked stale', () async { - OutboundConnection oc = OutboundConnectionImpl( - createMockAtServerSocket('test.test.test', 12345)); - OutboundMessageListener oml = OutboundMessageListener(oc); - expect((oc.getSocket() as MockSecureSocket).destroyed, false); - expect(oc.metaData?.isClosed, false); - oc.metaData!.isStale = true; + AtConnection oc = + AtSocketConnection(createMockAtServerSocket('test.test.test', 12345)); + AtMessageListener oml = AtMessageListener(oc); + expect((oc.underlying as MockSecureSocket).destroyed, false); + expect(oc.metaData.isClosed, false); + oc.metaData.isStale = true; await oml.closeConnection(); - expect((oc.getSocket() as MockSecureSocket).destroyed, true); - expect(oc.metaData?.isClosed, true); + expect((oc.underlying as MockSecureSocket).destroyed, true); + expect(oc.metaData.isClosed, true); }); }); @@ -195,9 +266,8 @@ void main() { Socket mockSocket = MockSecureSocket(); when(() => mockSocket.setOption(SocketOption.tcpNoDelay, true)) .thenAnswer((_) => true); - OutboundConnection connection = OutboundConnectionImpl(mockSocket); - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(connection); + AtConnection connection = AtSocketConnection(mockSocket); + AtMessageListener atMessageListener = AtMessageListener(connection); // We want to set up a connection, then call read() and have it time out. // When read() times out, the connection should be closed BEFORE the exception is thrown @@ -206,11 +276,11 @@ void main() { // This variable enables us to introduce a delay before closing the connection // The introduction of this delay enables the race condition (if it exists) to occur in this test if (delayBeforeClose != null) { - outboundMessageListener.delayBeforeClose = delayBeforeClose; + atMessageListener.delayBeforeClose = delayBeforeClose; } int transientWaitTimeMillis = 50; try { - await outboundMessageListener.read( + await atMessageListener.read( transientWaitTimeMillis: transientWaitTimeMillis); } on AtTimeoutException catch (expected) { expect( @@ -225,9 +295,8 @@ void main() { Socket mockSocket = MockSecureSocket(); when(() => mockSocket.setOption(SocketOption.tcpNoDelay, true)) .thenAnswer((_) => true); - OutboundConnection connection = OutboundConnectionImpl(mockSocket); - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(connection); + AtConnection connection = AtSocketConnection(mockSocket); + AtMessageListener atMessageListener = AtMessageListener(connection); // We want to set up a connection, then call read() and have it time out. // When read() times out, the connection should be closed BEFORE the exception is thrown @@ -236,12 +305,11 @@ void main() { // This variable enables us to introduce a delay before closing the connection // The introduction of this delay enables the race condition (if it exists) to occur in this test if (delayBeforeClose != null) { - outboundMessageListener.delayBeforeClose = delayBeforeClose; + atMessageListener.delayBeforeClose = delayBeforeClose; } int maxWaitMilliSeconds = 50; try { - await outboundMessageListener.read( - maxWaitMilliSeconds: maxWaitMilliSeconds); + await atMessageListener.read(maxWaitMilliSeconds: maxWaitMilliSeconds); expect(false, true, reason: 'Test should not have reached this point'); } on AtTimeoutException catch (expected) { expect(expected.message, @@ -254,9 +322,8 @@ void main() { Socket mockSocket = MockSecureSocket(); when(() => mockSocket.setOption(SocketOption.tcpNoDelay, true)) .thenAnswer((_) => true); - OutboundConnection connection = OutboundConnectionImpl(mockSocket); - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(connection); + AtConnection connection = AtSocketConnection(mockSocket); + AtMessageListener atMessageListener = AtMessageListener(connection); // We want to set up a connection, then call read() and have it time out. // When read() times out, the connection should be closed BEFORE the exception is thrown @@ -265,12 +332,11 @@ void main() { // This variable enables us to introduce a delay before closing the connection // The introduction of this delay enables the race condition (if it exists) to occur in this test if (delayBeforeClose != null) { - outboundMessageListener.delayBeforeClose = delayBeforeClose; + atMessageListener.delayBeforeClose = delayBeforeClose; } int maxWaitMilliSeconds = 50; try { - await outboundMessageListener.read( - maxWaitMilliSeconds: maxWaitMilliSeconds); + await atMessageListener.read(maxWaitMilliSeconds: maxWaitMilliSeconds); expect(false, true, reason: 'Test should not have reached this point'); } on AtTimeoutException catch (expected) { expect(expected.message, @@ -283,19 +349,19 @@ void main() { group('A group of tests to detect race condition in connection management', () { test( - 'Test that isInvalid is set on the OutboundConnection after transientWaitTime timeout BEFORE the OutboundMessageListener.read() returns', + 'Test that isInvalid is set on the AtConnection after transientWaitTime timeout BEFORE the AtMessageListener.read() returns', () async { await testOne(Duration(milliseconds: 100)); }); test( - 'Test that isInvalid is set on the OutboundConnection after maxWaitTime timeout BEFORE the OutboundMessageListener.read() returns', + 'Test that isInvalid is set on the AtConnection after maxWaitTime timeout BEFORE the AtMessageListener.read() returns', () async { await testTwo(Duration(milliseconds: 100)); }); test( - 'Test that an attempt to write to an outbound connection which has had a timeout will throw a ConnectionInvalidException', + 'Test that an attempt to write to an AtConnection which has had a timeout will throw a ConnectionInvalidException', () async { await testThree(Duration(milliseconds: 100)); }); @@ -303,22 +369,22 @@ void main() { /// These tests will pass even when the race condition exists because of complications in the event loop from testing /// The tests are here to verify that we haven't caused another problem from the introduction - /// of `@visibleForTesting Duration? delayBeforeClose` into OutboundMessageListener + /// of `@visibleForTesting Duration? delayBeforeClose` into AtMessageListener group('Same race condition tests without the artificial delay', () { test( - 'Test that isInvalid is set on the OutboundConnection after transientWaitTime timeout BEFORE the OutboundMessageListener.read() returns', + 'Test that isInvalid is set on the AtConnection after transientWaitTime timeout BEFORE the AtMessageListener.read() returns', () async { await testOne(null); }); test( - 'Test that isInvalid is set on the OutboundConnection after maxWaitTime timeout BEFORE the OutboundMessageListener.read() returns', + 'Test that isInvalid is set on the AtConnection after maxWaitTime timeout BEFORE the AtMessageListener.read() returns', () async { await testTwo(null); }); test( - 'Test that an attempt to write to an outbound connection which has had a timeout will throw a ConnectionInvalidException', + 'Test that an attempt to write to an AtConnection which has had a timeout will throw a ConnectionInvalidException', () async { await testThree(null); }); diff --git a/packages/at_lookup/test/outbound_message_listener_test.dart b/packages/at_lookup/test/outbound_message_listener_test.dart index a2c7a94c..46c7b6f8 100644 --- a/packages/at_lookup/test/outbound_message_listener_test.dart +++ b/packages/at_lookup/test/outbound_message_listener_test.dart @@ -1,73 +1,69 @@ import 'dart:async'; import 'package:at_commons/at_commons.dart'; -import 'package:at_lookup/at_lookup.dart'; -import 'package:at_lookup/src/connection/outbound_message_listener.dart'; -import 'package:test/test.dart'; +import 'package:at_lookup/src/connection/at_connection.dart'; +import 'package:at_lookup/src/connection/at_message_listener.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; import 'at_lookup_test_utils.dart'; void main() { - OutboundConnection mockOutBoundConnection = MockOutboundConnectionImpl(); + AtConnection mockAtConnection = MockAtSocketConnection(); - group('A group of tests to verify buffer of outbound message listener', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection); + group('A group of tests to verify buffer of AtMessageListener', () { + AtMessageListener atMessageListener = AtMessageListener(mockAtConnection); test('A test to validate complete data comes in single packet', () async { - await outboundMessageListener - .messageHandler('data:phone@alice\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:phone@alice\n@alice@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:phone@alice'); }); test( 'A test to validate complete data comes in packet and prompt in different packet', () async { - await outboundMessageListener - .messageHandler('data:@bob:phone@alice\n'.codeUnits); - await outboundMessageListener.messageHandler('@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:@bob:phone@alice\n'.codeUnits); + atMessageListener.messageHandler('@alice@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:@bob:phone@alice'); }); test('A test to validate data two complete data comes in single packets', () async { - await outboundMessageListener + atMessageListener .messageHandler('data:@bob:phone@alice\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'data:@bob:phone@alice'); - await outboundMessageListener + atMessageListener .messageHandler('data:public:phone@alice\n@alice@'.codeUnits); - response = await outboundMessageListener.read(); + response = await atMessageListener.read(); expect(response, 'data:public:phone@alice'); }); test('A test to validate data two complete data comes in multiple packets', () async { - await outboundMessageListener + atMessageListener .messageHandler('data:public:phone@alice\n@ali'.codeUnits); - await outboundMessageListener.messageHandler('ce@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('ce@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:public:phone@alice'); - await outboundMessageListener.messageHandler( + atMessageListener.messageHandler( 'data:@bob:location@alice,@bob:phone@alice\n@alice@'.codeUnits); - response = await outboundMessageListener.read(); + response = await atMessageListener.read(); expect(response, 'data:@bob:location@alice,@bob:phone@alice'); }); test('A test to validate single data comes two packets', () async { - await outboundMessageListener - .messageHandler('data:public:phone@'.codeUnits); - await outboundMessageListener.messageHandler('alice\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:public:phone@'.codeUnits); + atMessageListener.messageHandler('alice\n@alice@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:public:phone@alice'); }); test('A test to validate data contains @', () async { - await outboundMessageListener + atMessageListener .messageHandler('data:phone@alice_12345675\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'data:phone@alice_12345675'); }); @@ -75,71 +71,68 @@ void main() { 'A test to validate data contains @ and partial prompt of previous data', () async { // partial response of previous data. - await outboundMessageListener.messageHandler('data:hello\n@'.codeUnits); - await outboundMessageListener.messageHandler('alice@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:hello\n@'.codeUnits); + atMessageListener.messageHandler('alice@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:hello'); - await outboundMessageListener + atMessageListener .messageHandler('data:phone@alice_12345675\n@alice@'.codeUnits); - response = await outboundMessageListener.read(); + response = await atMessageListener.read(); expect(response, 'data:phone@alice_12345675'); }); test('A test to validate data contains new line character', () async { - await outboundMessageListener.messageHandler( + atMessageListener.messageHandler( 'data:value_contains_\nin_the_value\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'data:value_contains_\nin_the_value'); }); test('A test to validate data contains new line character and @', () async { - await outboundMessageListener.messageHandler( + atMessageListener.messageHandler( 'data:the_key_is\n@bob:phone@alice\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'data:the_key_is\n@bob:phone@alice'); }); }); group('A group of test to verify response from unauthorized connection', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection); + AtMessageListener atMessageListener = AtMessageListener(mockAtConnection); test('A test to validate response from unauthorized connection', () async { - await outboundMessageListener.messageHandler('data:hello\n@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:hello\n@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:hello'); }); test('A test to validate multiple response from unauthorized connection', () async { - await outboundMessageListener.messageHandler('data:hello\n@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:hello\n@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:hello'); - await outboundMessageListener.messageHandler('data:hi\n@'.codeUnits); - response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:hi\n@'.codeUnits); + response = await atMessageListener.read(); expect(response, 'data:hi'); }); test( 'A test to validate response from unauthorized connection in multiple packets', () async { - await outboundMessageListener - .messageHandler('data:public:location@alice,'.codeUnits); - await outboundMessageListener - .messageHandler('public:phone@alice\n@'.codeUnits); - var response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:public:location@alice,'.codeUnits); + atMessageListener.messageHandler('public:phone@alice\n@'.codeUnits); + var response = await atMessageListener.read(); expect(response, 'data:public:location@alice,public:phone@alice'); - await outboundMessageListener.messageHandler('data:hi\n@'.codeUnits); - response = await outboundMessageListener.read(); + atMessageListener.messageHandler('data:hi\n@'.codeUnits); + response = await atMessageListener.read(); expect(response, 'data:hi'); }); }); group('A group of test to validate buffer over flow scenarios', () { test('A test to verify buffer over flow exception', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection, bufferCapacity: 10); + AtMessageListener atMessageListener = + AtMessageListener(mockAtConnection, bufferCapacity: 10); expect( - () => outboundMessageListener + () => atMessageListener .messageHandler('data:dummy_data_to_exceed_limit'.codeUnits), throwsA(predicate((dynamic e) => e is BufferOverFlowException && @@ -148,11 +141,11 @@ void main() { }); test('A test to verify buffer over flow with multiple data packets', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection, bufferCapacity: 20); - outboundMessageListener.messageHandler('data:dummy_data'.codeUnits); + AtMessageListener atMessageListener = + AtMessageListener(mockAtConnection, bufferCapacity: 20); + atMessageListener.messageHandler('data:dummy_data'.codeUnits); expect( - () => outboundMessageListener + () => atMessageListener .messageHandler('to_exceed_limit\n@alice@'.codeUnits), throwsA(predicate((dynamic e) => e is BufferOverFlowException && @@ -163,37 +156,34 @@ void main() { group('A group of tests to verify error: and stream responses from server', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection); + AtMessageListener atMessageListener = AtMessageListener(mockAtConnection); test('A test to validate complete error comes in single packet', () async { - await outboundMessageListener.messageHandler( + atMessageListener.messageHandler( 'error:AT0012: Invalid value found\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'error:AT0012: Invalid value found'); }); test('A test to validate complete error comes in single packet', () async { - await outboundMessageListener + atMessageListener .messageHandler('stream:@bob:phone@alice\n@alice@'.codeUnits); - var response = await outboundMessageListener.read(); + var response = await atMessageListener.read(); expect(response, 'stream:@bob:phone@alice'); }); }); group('A group of tests to verify AtTimeOutException', () { - OutboundMessageListener outboundMessageListener = - OutboundMessageListener(mockOutBoundConnection); + AtMessageListener atMessageListener = AtMessageListener(mockAtConnection); setUp(() { - when(() => mockOutBoundConnection.isInValid()).thenAnswer((_) => false); - when(() => mockOutBoundConnection.close()) + when(() => mockAtConnection.isInValid()).thenAnswer((_) => false); + when(() => mockAtConnection.close()) .thenAnswer((Invocation invocation) async {}); }); test( 'A test to verify when no data is received from server within transientWaitTimeMillis', () async { expect( - () async => - await outboundMessageListener.read(transientWaitTimeMillis: 50), + () async => await atMessageListener.read(transientWaitTimeMillis: 50), throwsA(predicate((dynamic e) => e is AtTimeoutException && e.message @@ -205,7 +195,7 @@ void main() { expect( () async => // we want to trigger the maxWaitMilliSeconds exception, so setting transient to a higher value - await outboundMessageListener.read( + await atMessageListener.read( transientWaitTimeMillis: 100, maxWaitMilliSeconds: 50), throwsA(predicate((dynamic e) => e is AtTimeoutException && @@ -215,12 +205,10 @@ void main() { test( 'A test to verify partial response - wait time greater than transientWaitTimeMillis', () async { - await outboundMessageListener - .messageHandler('data:public:phone@'.codeUnits); - await outboundMessageListener.messageHandler('12'.codeUnits); + atMessageListener.messageHandler('data:public:phone@'.codeUnits); + atMessageListener.messageHandler('12'.codeUnits); expect( - () async => - await outboundMessageListener.read(transientWaitTimeMillis: 50), + () async => await atMessageListener.read(transientWaitTimeMillis: 50), throwsA(predicate((dynamic e) => e is AtTimeoutException && e.message @@ -229,16 +217,15 @@ void main() { test( 'A test to verify partial response - wait time greater than maxWaitMillis', () async { - await outboundMessageListener - .messageHandler('data:public:phone@'.codeUnits); - await outboundMessageListener.messageHandler('12'.codeUnits); - await outboundMessageListener.messageHandler('34'.codeUnits); - await outboundMessageListener.messageHandler('56'.codeUnits); - await outboundMessageListener.messageHandler('78'.codeUnits); + atMessageListener.messageHandler('data:public:phone@'.codeUnits); + atMessageListener.messageHandler('12'.codeUnits); + atMessageListener.messageHandler('34'.codeUnits); + atMessageListener.messageHandler('56'.codeUnits); + atMessageListener.messageHandler('78'.codeUnits); expect( () async => // we want to trigger the maxWaitMilliSeconds exception, so setting transient to a higher value - await outboundMessageListener.read( + await atMessageListener.read( transientWaitTimeMillis: 30, maxWaitMilliSeconds: 20), throwsA(predicate((dynamic e) => e is AtTimeoutException && @@ -249,21 +236,21 @@ void main() { 'A test to verify full response received - delay between messages from server', () async { String? response; - unawaited(outboundMessageListener + unawaited(atMessageListener .read(transientWaitTimeMillis: 50) .whenComplete(() => {}) .then((value) => response = value)); - await outboundMessageListener.messageHandler('data:'.codeUnits); + atMessageListener.messageHandler('data:'.codeUnits); await Future.delayed(Duration(milliseconds: 25)); - await outboundMessageListener.messageHandler('12'.codeUnits); + atMessageListener.messageHandler('12'.codeUnits); await Future.delayed(Duration(milliseconds: 15)); - await outboundMessageListener.messageHandler('34'.codeUnits); + atMessageListener.messageHandler('34'.codeUnits); await Future.delayed(Duration(milliseconds: 17)); - await outboundMessageListener.messageHandler('56'.codeUnits); + atMessageListener.messageHandler('56'.codeUnits); await Future.delayed(Duration(milliseconds: 30)); - await outboundMessageListener.messageHandler('78'.codeUnits); + atMessageListener.messageHandler('78'.codeUnits); await Future.delayed(Duration(milliseconds: 45)); - await outboundMessageListener.messageHandler('910\n@'.codeUnits); + atMessageListener.messageHandler('910\n@'.codeUnits); await Future.delayed(Duration(milliseconds: 25)); expect(response, isNotEmpty); expect(response, 'data:12345678910'); @@ -272,24 +259,24 @@ void main() { 'A test to verify max wait timeout - delay between messages from server', () async { String? response; - await outboundMessageListener + await atMessageListener .read(maxWaitMilliSeconds: 100) .catchError((e) { return e.toString(); }) .whenComplete(() => {}) .then((value) => {response = value}); - await outboundMessageListener.messageHandler('data:'.codeUnits); + atMessageListener.messageHandler('data:'.codeUnits); await Future.delayed(Duration(milliseconds: 15)); - await outboundMessageListener.messageHandler('12'.codeUnits); + atMessageListener.messageHandler('12'.codeUnits); await Future.delayed(Duration(milliseconds: 10)); - await outboundMessageListener.messageHandler('34'.codeUnits); + atMessageListener.messageHandler('34'.codeUnits); await Future.delayed(Duration(milliseconds: 12)); - await outboundMessageListener.messageHandler('56'.codeUnits); + atMessageListener.messageHandler('56'.codeUnits); await Future.delayed(Duration(milliseconds: 13)); - await outboundMessageListener.messageHandler('78'.codeUnits); + atMessageListener.messageHandler('78'.codeUnits); await Future.delayed(Duration(milliseconds: 20)); - await outboundMessageListener.messageHandler('910'.codeUnits); + atMessageListener.messageHandler('910'.codeUnits); await Future.delayed(Duration(milliseconds: 50)); expect(response, isNotEmpty); expect( @@ -302,24 +289,24 @@ void main() { 'A test to verify transient timeout - delay between messages from server', () async { String? response; - await outboundMessageListener + await atMessageListener .read(transientWaitTimeMillis: 50) .catchError((e) { return e.toString(); }) .whenComplete(() => {}) .then((value) => {response = value}); - await outboundMessageListener.messageHandler('data:'.codeUnits); + atMessageListener.messageHandler('data:'.codeUnits); await Future.delayed(Duration(milliseconds: 10)); - await outboundMessageListener.messageHandler('12'.codeUnits); + atMessageListener.messageHandler('12'.codeUnits); await Future.delayed(Duration(milliseconds: 15)); - await outboundMessageListener.messageHandler('34'.codeUnits); + atMessageListener.messageHandler('34'.codeUnits); await Future.delayed(Duration(milliseconds: 17)); - await outboundMessageListener.messageHandler('56'.codeUnits); + atMessageListener.messageHandler('56'.codeUnits); await Future.delayed(Duration(milliseconds: 20)); - await outboundMessageListener.messageHandler('78'.codeUnits); + atMessageListener.messageHandler('78'.codeUnits); await Future.delayed(Duration(milliseconds: 10)); - await outboundMessageListener.messageHandler('910'.codeUnits); + atMessageListener.messageHandler('910'.codeUnits); await Future.delayed(Duration(milliseconds: 60)); expect(response, isNotEmpty); expect( diff --git a/packages/at_lookup/test/secondary_address_cache_test.dart b/packages/at_lookup/test/secondary_address_cache_test.dart index 326220f3..955cff15 100644 --- a/packages/at_lookup/test/secondary_address_cache_test.dart +++ b/packages/at_lookup/test/secondary_address_cache_test.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:at_commons/at_commons.dart'; import 'package:at_lookup/at_lookup.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; -import 'package:mocktail/mocktail.dart'; import 'at_lookup_test_utils.dart'; @@ -141,7 +141,7 @@ void main() async { late Function socketOnDataFn; late SecureSocket mockSocket; - late MockSecureSocketFactory mockSocketFactory; + late MockAtLookupConnectionFactory mockSocketFactory; late CacheableSecondaryAddressFinder cachingAtServerFinder; @@ -162,7 +162,7 @@ void main() async { setUp(() { mockSocket = createMockAtDirectorySocket(mockAtDirectoryHost, 64); - mockSocketFactory = MockSecureSocketFactory(); + mockSocketFactory = MockAtLookupConnectionFactory(); cachingAtServerFinder = CacheableSecondaryAddressFinder( mockAtDirectoryHost, 64, @@ -170,9 +170,8 @@ void main() async { socketFactory: mockSocketFactory)); numSocketCreateCalls = 0; - when(() => - mockSocketFactory.createSocket(mockAtDirectoryHost, '64', any())) - .thenAnswer((invocation) { + when(() => mockSocketFactory.createUnderlying( + mockAtDirectoryHost, '64', any())).thenAnswer((invocation) { print( 'mock create socket: numFailures $numSocketCreateCalls requiredFailures $requiredFailures'); if (numSocketCreateCalls++ < requiredFailures) { diff --git a/tests/at_onboarding_cli_functional_tests/pubspec.yaml b/tests/at_onboarding_cli_functional_tests/pubspec.yaml index 1e6171f4..d8a22440 100644 --- a/tests/at_onboarding_cli_functional_tests/pubspec.yaml +++ b/tests/at_onboarding_cli_functional_tests/pubspec.yaml @@ -19,6 +19,13 @@ dependency_overrides: path: ../../packages/at_onboarding_cli at_commons: path: ../../packages/at_commons + at_lookup: + path: ../../packages/at_lookup + at_client: + git: + url: https://github.com/atsign-foundation/at_client_sdk + path: packages/at_client + ref: websocket_test at_chops: path: ../../packages/at_chops at_cli_commons: diff --git a/tests/at_onboarding_cli_functional_tests/test/at_lookup_test.dart b/tests/at_onboarding_cli_functional_tests/test/at_lookup_test.dart new file mode 100644 index 00000000..10ae42b6 --- /dev/null +++ b/tests/at_onboarding_cli_functional_tests/test/at_lookup_test.dart @@ -0,0 +1,117 @@ +import 'package:at_chops/at_chops.dart'; +import 'package:at_commons/at_builders.dart'; +import 'package:at_commons/at_commons.dart'; +import 'package:at_demo_data/at_demo_data.dart' as at_demos; +import 'package:at_lookup/at_lookup.dart'; +import 'package:test/test.dart'; + +void main() { + String atSign = '@bob🛠'; + AtChops atChopsKeys = createAtChopsFromDemoKeys(atSign); + + group('A group of tests to assert on authenticate functionality', () { + test( + 'A test to verify a secure socket connection and do a cram authenticate and scan', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupSecureSocketFactory()); + await atLookup.cramAuthenticate(at_demos.cramKeyMap[atSign]!); + var command = 'scan\n'; + var response = await atLookup.executeCommand(command, auth: true); + expect(response, contains('public:signing_publickey$atSign')); + }, timeout: Timeout(Duration(minutes: 5))); + + test( + 'A test to verify a websocket connection and do a cram authenticate and scan', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupWebSocketFactory()); + await atLookup.cramAuthenticate(at_demos.cramKeyMap[atSign]!); + var command = 'scan\n'; + var response = await atLookup.executeCommand(command, auth: true); + expect(response, contains('public:signing_publickey$atSign')); + }); + + test( + 'A test to verify a socket connection by passing useWebSocket to false and do a cram authenticate and scan', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupWebSocketFactory()); + await atLookup.cramAuthenticate(at_demos.cramKeyMap[atSign]!); + var command = 'scan\n'; + var response = await atLookup.executeCommand(command, auth: true); + expect(response, contains('public:signing_publickey$atSign')); + }); + + test( + 'A test to verify a websocket connection and do a cram authenticate and update', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupWebSocketFactory()); + await atLookup.cramAuthenticate(at_demos.cramKeyMap[atSign]!); + // update public and private keys manually + var command = + 'update:privatekey:at_pkam_publickey ${at_demos.pkamPublicKeyMap[atSign]}\n'; + var response = await atLookup.executeCommand(command, auth: true); + expect(response, 'data:-1'); + command = + 'update:public:publickey${atSign} ${at_demos.encryptionPublicKeyMap[atSign]}\n'; + await atLookup.executeCommand(command, auth: true); + print(response); + assert((!response!.contains('Invalid syntax')) && + (!response.contains('null'))); + }); + + test( + 'A test to verify a websocket connection and do a pkam authenticate and executeCommand', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atChops = atChopsKeys; + await atLookup.pkamAuthenticate(); + var command = 'update:public:username$atSign bob123\n'; + var response = await atLookup.executeCommand(command, auth: true); + assert((!response!.contains('Invalid syntax')) && + (!response.contains('null'))); + }); + + test( + 'A test to verify a websocket connection and do a pkam authenticate and execute verb', + () async { + var atLookup = AtLookupImpl(atSign, 'vip.ve.atsign.zone', 64, + atConnectionFactory: AtLookupWebSocketFactory()); + atLookup.atChops = atChopsKeys; + await atLookup.pkamAuthenticate(); + var atKey = 'key1'; + String value = 'value1'; + var updateBuilder = UpdateVerbBuilder() + ..value = 'value1' + ..atKey = (AtKey() + ..key = atKey + ..sharedBy = atSign + ..metadata = (Metadata()..isPublic = true)); + var response = await atLookup.executeVerb(updateBuilder); + print(response); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + var llookupVerbBuilder = LLookupVerbBuilder() + ..atKey = (AtKey() + ..key = atKey + ..sharedBy = atSign + ..metadata = (Metadata()..isPublic = true)); + response = await atLookup.executeVerb(llookupVerbBuilder); + expect(response, contains(value)); + }, timeout: Timeout(Duration(minutes: 5))); + }); +} + +AtChops createAtChopsFromDemoKeys(String atSign) { + var atEncryptionKeyPair = AtEncryptionKeyPair.create( + at_demos.encryptionPublicKeyMap[atSign]!, + at_demos.encryptionPrivateKeyMap[atSign]!); + var atPkamKeyPair = AtPkamKeyPair.create( + at_demos.pkamPublicKeyMap[atSign]!, at_demos.pkamPrivateKeyMap[atSign]!); + final atChopsKeys = AtChopsKeys.create(atEncryptionKeyPair, atPkamKeyPair); + atChopsKeys.selfEncryptionKey = AESKey(at_demos.aesKeyMap[atSign]!); + return AtChopsImpl(atChopsKeys); +}