mumbullet/test/integration/connection_test.dart

273 lines
8.8 KiB
Dart

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<bool>();
// 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<bool>();
// 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<bool>();
final reconnectionCompleter = Completer<bool>();
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);
});
}