feat: import servers from ~/.ssh/config (#873)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-31 19:33:29 +08:00
committed by GitHub
parent a97b3cf43e
commit 12a243d139
42 changed files with 2850 additions and 334 deletions

355
test/ssh_config_test.dart Normal file
View File

@@ -0,0 +1,355 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/ssh_config.dart';
void main() {
group('SSHConfig Tests', () {
late Directory tempDir;
late File configFile;
setUp(() async {
// Create temporary directory for test SSH config files
tempDir = await Directory.systemTemp.createTemp('ssh_config_test');
configFile = File('${tempDir.path}/config');
});
tearDown(() async {
// Clean up temporary files
if (tempDir.existsSync()) {
await tempDir.delete(recursive: true);
}
});
test('configExists returns false for non-existent file', () async {
final (_, exists) = SSHConfig.configExists('/non/existent/path');
expect(exists, false);
});
test('configExists returns true for existing file', () async {
await configFile.writeAsString('Host example\n HostName example.com\n');
final (_, exists) = SSHConfig.configExists(configFile.path);
expect(exists, true);
});
test('parseConfig handles empty file', () async {
await configFile.writeAsString('');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty);
});
test('parseConfig handles file with only comments', () async {
await configFile.writeAsString('''
# This is a comment
# Another comment
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty);
});
test('parseConfig parses single host correctly', () async {
await configFile.writeAsString('''
Host myserver
HostName 192.168.1.100
User admin
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 2222);
});
test('parseConfig handles missing HostName', () async {
await configFile.writeAsString('''
Host myserver
User admin
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty); // Should skip hosts without HostName
});
test('parseConfig uses defaults for missing optional fields', () async {
await configFile.writeAsString('''
Host simple
HostName example.com
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'simple');
expect(server.ip, 'example.com');
expect(server.user, 'root'); // default user
expect(server.port, 22); // default port
});
test('parseConfig handles multiple hosts', () async {
await configFile.writeAsString('''
Host server1
HostName 192.168.1.100
User alice
Port 22
Host server2
HostName 192.168.1.200
User bob
Port 2222
Host server3
HostName example.com
User charlie
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(3));
expect(servers[0].name, 'server1');
expect(servers[0].ip, '192.168.1.100');
expect(servers[0].user, 'alice');
expect(servers[0].port, 22);
expect(servers[1].name, 'server2');
expect(servers[1].ip, '192.168.1.200');
expect(servers[1].user, 'bob');
expect(servers[1].port, 2222);
expect(servers[2].name, 'server3');
expect(servers[2].ip, 'example.com');
expect(servers[2].user, 'charlie');
expect(servers[2].port, 22);
});
test('parseConfig handles case insensitive keywords', () async {
await configFile.writeAsString('''
host myserver
hostname 192.168.1.100
user admin
port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 2222);
});
test('parseConfig handles comments and empty lines', () async {
await configFile.writeAsString('''
# Global settings
Host *
ServerAliveInterval 60
# My development server
Host devserver
HostName 192.168.1.50
User developer # development user
Port 22
# Empty line below
Host prodserver
HostName 10.0.0.100
User production
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(2));
expect(servers[0].name, 'devserver');
expect(servers[0].ip, '192.168.1.50');
expect(servers[0].user, 'developer');
expect(servers[1].name, 'prodserver');
expect(servers[1].ip, '10.0.0.100');
expect(servers[1].user, 'production');
});
test('parseConfig handles wildcard hosts', () async {
await configFile.writeAsString('''
Host *
User defaultuser
Port 2222
Host myserver
HostName 192.168.1.100
User admin
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1)); // Only named hosts, not wildcards
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 22); // Uses default, not wildcard setting
});
test('parseConfig handles IdentityFile', () async {
await configFile.writeAsString('''
Host keyserver
HostName 192.168.1.100
User admin
IdentityFile ~/.ssh/special_key
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.keyId, '~/.ssh/special_key');
});
test('parseConfig handles quoted values', () async {
await configFile.writeAsString('''
Host "server with spaces"
HostName "192.168.1.100"
User "admin user"
IdentityFile "~/.ssh/key with spaces"
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'server with spaces');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin user');
expect(server.keyId, '~/.ssh/key with spaces');
});
test('parseConfig handles invalid port values', () async {
await configFile.writeAsString('''
Host badport
HostName 192.168.1.100
Port notanumber
Host goodserver
HostName 192.168.1.200
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(2));
// First server should use default port due to invalid port
expect(servers[0].name, 'badport');
expect(servers[0].port, 22); // default port
// Second server should use specified port
expect(servers[1].name, 'goodserver');
expect(servers[1].port, 2222);
});
test('parseConfig skips hosts with multiple host patterns', () async {
await configFile.writeAsString('''
Host server1 server2
HostName 192.168.1.100
Host singleserver
HostName 192.168.1.200
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1)); // Only single host patterns
expect(servers[0].name, 'singleserver');
});
test('parseConfig handles ProxyJump (ignored)', () async {
await configFile.writeAsString('''
Host jumpserver
HostName 192.168.1.100
User admin
ProxyJump bastion.example.com
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'jumpserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
// ProxyJump is ignored in current implementation
});
test('parseConfig returns empty list for non-existent file', () async {
final servers = await SSHConfig.parseConfig('/non/existent/path');
expect(servers, isEmpty);
});
test('parseConfig handles real-world SSH config example', () async {
await configFile.writeAsString('''
# Default settings for all hosts
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
# Production servers
Host prod-web-01
HostName 10.0.1.100
User deploy
Port 22
IdentityFile ~/.ssh/production.pem
Host prod-db-01
HostName 10.0.1.200
User ubuntu
Port 2222
IdentityFile ~/.ssh/production.pem
# Development environment
Host dev
HostName dev.example.com
User developer
Port 22
# Jump host configuration
Host bastion
HostName bastion.example.com
User ec2-user
IdentityFile ~/.ssh/bastion.pem
Host internal-server
HostName 172.16.0.50
User admin
ProxyJump bastion
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(5));
// Check specific servers
final prodWeb = servers.firstWhere((s) => s.name == 'prod-web-01');
expect(prodWeb.ip, '10.0.1.100');
expect(prodWeb.user, 'deploy');
expect(prodWeb.port, 22);
expect(prodWeb.keyId, '~/.ssh/production.pem');
final prodDb = servers.firstWhere((s) => s.name == 'prod-db-01');
expect(prodDb.ip, '10.0.1.200');
expect(prodDb.user, 'ubuntu');
expect(prodDb.port, 2222);
final dev = servers.firstWhere((s) => s.name == 'dev');
expect(dev.ip, 'dev.example.com');
expect(dev.user, 'developer');
expect(dev.port, 22);
expect(dev.keyId, isNull);
});
});
}