import 'dart:async'; import 'dart:io'; import 'package:mumbullet/mumbullet.dart'; import 'package:test/test.dart'; /// Integration test for Mumble connection /// /// This test: /// - Requires Docker to be running /// - Uses a dedicated docker-compose.test.yml file /// - Creates a Mumble server with predefined channels /// - Tests connection, authentication, and channel navigation /// /// The server is automatically started before tests and stopped after tests /// using docker-compose commands. @Tags(['docker']) void main() { // Use setUpAll and tearDownAll to ensure Docker is only started/stopped once for all tests late MumbleConnection connection; late AppConfig config; const rootPath = '/home/nate/source/non-work/mumbullet'; const configPath = '$rootPath/test/fixtures/test_config.json'; setUpAll(() async { // Create cache directory if it doesn't exist final cacheDir = Directory('$rootPath/test/fixtures/cache'); if (!await cacheDir.exists()) { await cacheDir.create(recursive: true); } // Check if Docker is running try { final checkDocker = await Process.run('docker', ['info']); if (checkDocker.exitCode != 0) { fail('Docker is not running. Please start Docker and try again.'); } } catch (e) { fail('Failed to check Docker status: $e. Is Docker installed?'); } // Start Docker container using test-specific compose file print('Starting Mumble server container for tests...'); final dockerProcess = await Process.run( 'docker-compose', ['-f', 'test/integration/docker-compose.test.yml', 'up', '-d'], workingDirectory: rootPath ); if (dockerProcess.exitCode != 0) { fail('Failed to start Docker container: ${dockerProcess.stderr}'); } // Wait for server to start print('Waiting for Mumble server to start...'); await Future.delayed(Duration(seconds: 5)); }); tearDownAll(() async { // Stop Docker container print('Stopping Mumble server container...'); final stopProcess = await Process.run( 'docker-compose', ['-f', 'test/integration/docker-compose.test.yml', 'down', '-v'], workingDirectory: rootPath ); if (stopProcess.exitCode != 0) { print('Warning: Failed to stop Docker container: ${stopProcess.stderr}'); } }); setUp(() async { // Load test configuration config = AppConfig.fromFile(configPath); // Create connection connection = MumbleConnection(config.mumble); }); tearDown(() async { // Disconnect from server if (connection.isConnected) { await connection.disconnect(); } }); test('Should connect to Mumble server with password', () async { // Setup test expectations final connectionCompleter = Completer(); // Listen for connection state changes connection.connectionState.listen((isConnected) { if (isConnected && !connectionCompleter.isCompleted) { connectionCompleter.complete(true); } }); // Attempt to connect with password await connection.connect(); // Wait for connection or timeout expect( await connectionCompleter.future.timeout(Duration(seconds: 10), onTimeout: () => false), isTrue, reason: 'Failed to connect to Mumble server with password', ); // Verify connection is established expect(connection.isConnected, isTrue); // Verify we can get channel information expect(connection.client, isNotNull); expect(connection.client!.rootChannel, isNotNull); // Verify we can send a message await connection.sendChannelMessage('Integration test message'); }); test('Should join different channels', () async { // Setup test expectations final connectionCompleter = Completer(); // Listen for connection state changes connection.connectionState.listen((isConnected) { if (isConnected && !connectionCompleter.isCompleted) { connectionCompleter.complete(true); } }); // Connect to server await connection.connect(); // Wait for connection expect(await connectionCompleter.future.timeout(Duration(seconds: 10), onTimeout: () => false), isTrue); expect(connection.client, isNotNull); final channels = connection.client?.getChannels(); print(channels?.values); expect(channels, isNotNull); expect(channels?.length, greaterThan(1)); // Test joining the Music channel await connection.joinChannel('Music'); expect(connection.currentChannel?.name, equals('Music')); // Test joining a subchannel await connection.joinChannel('Subroom1'); expect(connection.currentChannel?.name, equals('Subroom1')); // Test joining another top-level channel await connection.joinChannel('General'); expect(connection.currentChannel?.name, equals('General')); // Test joining the Gaming channel await connection.joinChannel('Gaming'); expect(connection.currentChannel?.name, equals('Gaming')); // Test joining the root channel await connection.joinChannel(''); expect(connection.currentChannel?.name, equals('Root')); }); test('Should handle authentication failure', () async { // Create a connection with incorrect password final badConnection = MumbleConnection( MumbleConfig( server: config.mumble.server, port: config.mumble.port, username: config.mumble.username, password: 'wrongpassword', channel: config.mumble.channel, ), ); // Try to connect with wrong password try { await badConnection.connect(); fail('Should have failed to connect with wrong password'); } catch (e) { // Expected failure expect(badConnection.isConnected, isFalse); } finally { // Clean up await badConnection.disconnect(); } }); // test('Should allow multiple bots to connect simultaneously', () async { // // Load additional config // final adminConfig = AppConfig.fromFile('${rootPath}/test/fixtures/test_configs/admin_config.json'); // final userConfig = AppConfig.fromFile('${rootPath}/test/fixtures/test_configs/user_config.json'); // // Create additional connections // final adminConnection = MumbleConnection(adminConfig.mumble); // final userConnection = MumbleConnection(userConfig.mumble); // try { // // Connect all bots // await Future.wait([adminConnection.connect(), userConnection.connect()]); // // Verify all connections are established // expect(adminConnection.isConnected, isTrue); // expect(userConnection.isConnected, isTrue); // // Test sending messages from different bots // await adminConnection.sendChannelMessage('Admin bot test message'); // await userConnection.sendChannelMessage('User bot test message'); // // Test joining different channels with each bot // await adminConnection.joinChannel('Gaming'); // await userConnection.joinChannel('General'); // expect(adminConnection.currentChannel?.name, equals('Gaming')); // expect(userConnection.currentChannel?.name, equals('General')); // } finally { // // Clean up // await adminConnection.disconnect(); // await userConnection.disconnect(); // } // }); test('Should handle reconnection', () async { // Setup test expectations final connectionCompleter = Completer(); final reconnectionCompleter = Completer(); var initiallyConnected = false; // Listen for connection state changes connection.connectionState.listen((isConnected) { if (isConnected && !initiallyConnected) { initiallyConnected = true; connectionCompleter.complete(true); } else if (isConnected && initiallyConnected && !reconnectionCompleter.isCompleted) { reconnectionCompleter.complete(true); } }); // Connect initially await connection.connect(); // Wait for initial connection expect( await connectionCompleter.future.timeout(Duration(seconds: 10), onTimeout: () => false), isTrue, reason: 'Failed to connect to Mumble server initially', ); // Restart the Mumble server print('Restarting Mumble server container to test reconnection...'); final restartProcess = await Process.run( 'docker-compose', ['-f', 'test/integration/docker-compose.test.yml', 'restart', 'mumble'], workingDirectory: rootPath ); if (restartProcess.exitCode != 0) { fail('Failed to restart Mumble server: ${restartProcess.stderr}'); } // Wait for reconnection await expectLater( reconnectionCompleter.future.timeout(Duration(seconds: 20), onTimeout: () => false), isTrue, reason: 'Failed to reconnect to Mumble server after restart', ); // Verify connection is re-established expect(connection.isConnected, isTrue); }); }