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

414
test/server_dedup_test.dart Normal file
View File

@@ -0,0 +1,414 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/server_dedup.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
// Mock functions to test the deduplication logic without relying on ServerStore
List<Spi> _mockDeduplicateServers(List<Spi> importedServers, List<Spi> existingServers) {
final deduplicated = <Spi>[];
for (final imported in importedServers) {
// Check against existing servers
if (!_mockIsDuplicate(imported, existingServers)) {
// Also check against already processed imported servers
if (!_mockIsDuplicate(imported, deduplicated)) {
deduplicated.add(imported);
}
}
}
return deduplicated;
}
bool _mockIsDuplicate(Spi imported, List<Spi> existing) {
for (final existingSpi in existing) {
// Check for exact match on ip:port@user combination
if (existingSpi.ip == imported.ip &&
existingSpi.port == imported.port &&
existingSpi.user == imported.user) {
return true;
}
}
return false;
}
List<Spi> _mockResolveNameConflicts(List<Spi> importedServers, List<String> existingNames) {
final existingNamesSet = existingNames.toSet();
final processedNames = <String>{};
final result = <Spi>[];
for (final server in importedServers) {
String newName = server.name;
int suffix = 2;
// Check against both existing servers and already processed servers
while (existingNamesSet.contains(newName) || processedNames.contains(newName)) {
newName = '${server.name} ($suffix)';
suffix++;
}
processedNames.add(newName);
if (newName != server.name) {
result.add(server.copyWith(name: newName));
} else {
result.add(server);
}
}
return result;
}
void main() {
group('ServerDeduplication Tests', () {
late List<Spi> existingServers;
late List<Spi> importedServers;
setUp(() {
// Set up some existing servers for testing
existingServers = [
const Spi(
name: 'production-web',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: 'existing1',
),
const Spi(
name: 'staging-db',
ip: '192.168.1.200',
port: 22,
user: 'postgres',
id: 'existing2',
),
const Spi(
name: 'dev-server',
ip: '192.168.1.50',
port: 2222,
user: 'developer',
id: 'existing3',
),
];
});
test('deduplicateServers removes exact duplicates', () {
importedServers = [
const Spi(
name: 'new-server-1',
ip: '192.168.1.100',
port: 22,
user: 'root', // Same as existing1
),
const Spi(
name: 'new-server-2',
ip: '192.168.1.300',
port: 22,
user: 'admin', // New server
),
const Spi(
name: 'new-server-3',
ip: '192.168.1.200',
port: 22,
user: 'postgres', // Same as existing2
),
];
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(1));
expect(deduplicated.first.name, 'new-server-2');
expect(deduplicated.first.ip, '192.168.1.300');
});
test('deduplicateServers considers port and user in deduplication', () {
importedServers = [
const Spi(
name: 'same-ip-diff-port',
ip: '192.168.1.100',
port: 2222, // Different port
user: 'root',
),
const Spi(
name: 'same-ip-diff-user',
ip: '192.168.1.100',
port: 22,
user: 'admin', // Different user
),
const Spi(
name: 'exact-duplicate',
ip: '192.168.1.100',
port: 22,
user: 'root', // Exact duplicate
),
];
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(2));
expect(deduplicated.any((s) => s.name == 'same-ip-diff-port'), isTrue);
expect(deduplicated.any((s) => s.name == 'same-ip-diff-user'), isTrue);
expect(deduplicated.any((s) => s.name == 'exact-duplicate'), isFalse);
});
test('deduplicateServers handles empty existing servers list', () {
importedServers = [
const Spi(
name: 'server1',
ip: '192.168.1.100',
port: 22,
user: 'root',
),
const Spi(
name: 'server2',
ip: '192.168.1.200',
port: 22,
user: 'admin',
),
];
final deduplicated = _mockDeduplicateServers(importedServers, []);
expect(deduplicated, hasLength(2));
expect(deduplicated, equals(importedServers));
});
test('deduplicateServers handles empty imported servers list', () {
final deduplicated = _mockDeduplicateServers([], existingServers);
expect(deduplicated, isEmpty);
});
test('resolveNameConflicts appends numbers to conflicting names', () {
importedServers = [
const Spi(
name: 'production-web', // Conflicts with existing
ip: '192.168.1.300',
port: 22,
user: 'root',
),
const Spi(
name: 'dev-server', // Conflicts with existing
ip: '192.168.1.400',
port: 22,
user: 'root',
),
const Spi(
name: 'unique-name', // No conflict
ip: '192.168.1.500',
port: 22,
user: 'root',
),
];
final resolved = _mockResolveNameConflicts(
importedServers,
existingServers.map((s) => s.name).toList(),
);
expect(resolved, hasLength(3));
expect(resolved[0].name, 'production-web (2)');
expect(resolved[1].name, 'dev-server (2)');
expect(resolved[2].name, 'unique-name');
});
test('resolveNameConflicts handles multiple conflicts with same base name', () {
importedServers = [
const Spi(
name: 'server',
ip: '192.168.1.100',
port: 22,
user: 'root',
),
const Spi(
name: 'server',
ip: '192.168.1.200',
port: 22,
user: 'admin',
),
const Spi(
name: 'server',
ip: '192.168.1.300',
port: 2222,
user: 'root',
),
];
final existingNames = ['server', 'server (2)'];
final resolved = _mockResolveNameConflicts(importedServers, existingNames);
expect(resolved, hasLength(3));
expect(resolved[0].name, 'server (3)');
expect(resolved[1].name, 'server (4)');
expect(resolved[2].name, 'server (5)');
});
test('resolveNameConflicts handles empty input', () {
final resolved = _mockResolveNameConflicts([], ['existing1', 'existing2']);
expect(resolved, isEmpty);
});
test('getImportSummary calculates correct statistics', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
const Spi(name: 'duplicate', ip: '192.168.1.100', port: 22, user: 'root'), // Duplicate of server1
];
final deduplicatedList = [
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
deduplicatedList,
);
expect(summary.total, 4);
expect(summary.duplicates, 2); // server1 and duplicate
expect(summary.toImport, 2);
});
test('getImportSummary handles case with no duplicates', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
originalList,
);
expect(summary.total, 2);
expect(summary.duplicates, 0);
expect(summary.toImport, 2);
});
test('getImportSummary handles case with all duplicates', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
[],
);
expect(summary.total, 2);
expect(summary.duplicates, 2);
expect(summary.toImport, 0);
});
test('getImportSummary handles empty lists', () {
final summary = ServerDeduplication.getImportSummary([], []);
expect(summary.total, 0);
expect(summary.duplicates, 0);
expect(summary.toImport, 0);
});
test('complete deduplication workflow', () {
// Simulate a complete import workflow
importedServers = [
const Spi(
name: 'production-web', // Name conflicts with existing
ip: '192.168.1.400', // Different IP, so not a duplicate
port: 22,
user: 'root',
),
const Spi(
name: 'new-staging',
ip: '192.168.1.100', // Same as existing1, should be removed
port: 22,
user: 'root',
),
const Spi(
name: 'unique-server',
ip: '192.168.1.500', // Unique server
port: 22,
user: 'admin',
),
];
// Step 1: Remove duplicates
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(2)); // new-staging should be removed
// Step 2: Resolve name conflicts
final resolved = _mockResolveNameConflicts(
deduplicated,
existingServers.map((s) => s.name).toList(),
);
expect(resolved, hasLength(2));
expect(resolved.any((s) => s.name == 'production-web (2)'), isTrue);
expect(resolved.any((s) => s.name == 'unique-server'), isTrue);
// Step 3: Get summary
final summary = ServerDeduplication.getImportSummary(
importedServers,
resolved,
);
expect(summary.total, 3);
expect(summary.duplicates, 1);
expect(summary.toImport, 2);
});
test('deduplication key generation is consistent', () {
const server1 = Spi(
name: 'test1',
ip: '192.168.1.100',
port: 22,
user: 'root',
);
const server2 = Spi(
name: 'test2', // Different name
ip: '192.168.1.100', // Same IP
port: 22, // Same port
user: 'root', // Same user
);
final servers = [server1, server2];
final deduplicated = _mockDeduplicateServers(servers, []);
// First server should be kept, second should be removed since it has same ip:port@user
expect(deduplicated, hasLength(1));
expect(deduplicated.first.name, 'test1');
});
test('ImportSummary properties work correctly', () {
final summary = ServerDeduplication.getImportSummary(
[
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
],
[
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
],
);
expect(summary.total, 2);
expect(summary.duplicates, 1);
expect(summary.toImport, 1);
expect(summary.hasDuplicates, isTrue);
expect(summary.hasItemsToImport, isTrue);
});
test('ImportSummary with no duplicates or imports', () {
final summary = ServerDeduplication.getImportSummary([], []);
expect(summary.total, 0);
expect(summary.duplicates, 0);
expect(summary.toImport, 0);
expect(summary.hasDuplicates, isFalse);
expect(summary.hasItemsToImport, isFalse);
});
});
}

View File

@@ -0,0 +1,317 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
void main() {
group('Server Edit Page Logic Tests', () {
test('SSH import should only be available on desktop platforms', () {
final desktopPlatforms = ['linux', 'macos', 'windows'];
final mobilePlatforms = ['android', 'ios', 'fuchsia'];
for (final platform in desktopPlatforms) {
final isDesktop = desktopPlatforms.contains(platform);
expect(isDesktop, isTrue, reason: '$platform should support SSH import');
}
for (final platform in mobilePlatforms) {
final isDesktop = desktopPlatforms.contains(platform);
expect(isDesktop, isFalse, reason: '$platform should not support SSH import');
}
});
test('permission prompt conditions are correct', () {
// Test the conditions for showing permission prompt
// Should prompt when: firstTimeReadSSHCfg=true, sshConfigExists=true, isNewServer=true
bool shouldPrompt(bool firstTime, bool configExists, bool isNew) {
return firstTime && configExists && isNew;
}
expect(shouldPrompt(true, true, true), isTrue); // All conditions met
expect(shouldPrompt(false, true, true), isFalse); // Setting disabled
expect(shouldPrompt(true, false, true), isFalse); // No config file
expect(shouldPrompt(true, true, false), isFalse); // Editing existing server
expect(shouldPrompt(false, false, false), isFalse); // No conditions met
});
test('server validation logic works correctly', () {
// Test server validation without actual form widgets
// Valid server
const validServer = Spi(
name: 'test-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
);
expect(validServer.name.isNotEmpty, isTrue);
expect(validServer.ip.isNotEmpty, isTrue);
expect(validServer.port > 0 && validServer.port <= 65535, isTrue);
expect(validServer.user.isNotEmpty, isTrue);
// Invalid cases
expect(''.isNotEmpty, isFalse); // Empty name
expect(0 > 0, isFalse); // Invalid port
expect(65536 <= 65535, isFalse); // Port too high
});
test('server form data processing is correct', () {
// Test data processing logic
final Map<String, dynamic> formData = {
'name': 'my-server',
'ip': '192.168.1.100',
'port': '2222',
'user': 'admin',
};
// Process form data into server object
final server = Spi(
name: formData['name'] as String,
ip: formData['ip'] as String,
port: int.parse(formData['port'] as String),
user: formData['user'] as String,
);
expect(server.name, 'my-server');
expect(server.ip, '192.168.1.100');
expect(server.port, 2222);
expect(server.user, 'admin');
});
test('SSH key handling is correct', () {
// Test SSH key field handling
const serverWithKey = Spi(
name: 'key-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
keyId: '~/.ssh/id_rsa',
);
expect(serverWithKey.keyId, '~/.ssh/id_rsa');
expect(serverWithKey.keyId?.isNotEmpty, isTrue);
const serverWithoutKey = Spi(
name: 'pwd-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
pwd: 'password123',
);
expect(serverWithoutKey.keyId, isNull);
expect(serverWithoutKey.pwd, 'password123');
});
test('server editing vs creation logic', () {
// Test logic for distinguishing between editing and creating servers
const existingServer = Spi(
name: 'existing',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: 'server123',
);
// Existing server has non-empty ID
final isEditing = existingServer.id.isNotEmpty;
final isCreating = !isEditing;
expect(isEditing, isTrue);
expect(isCreating, isFalse);
const newServer = Spi(
name: 'new-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: '',
);
final isCreatingNew = newServer.id.isEmpty;
final isEditingExisting = !isCreatingNew;
expect(isCreatingNew, isTrue);
expect(isEditingExisting, isFalse);
});
test('form field population from imported server', () {
// Test that imported server data correctly populates form fields
const importedServer = Spi(
name: 'imported-prod-web',
ip: '10.0.1.100',
port: 2222,
user: 'deploy',
keyId: '~/.ssh/production.pem',
);
// Simulate form field population
final formFields = {
'name': importedServer.name,
'ip': importedServer.ip,
'port': importedServer.port.toString(),
'user': importedServer.user,
'keyId': importedServer.keyId,
};
expect(formFields['name'], 'imported-prod-web');
expect(formFields['ip'], '10.0.1.100');
expect(formFields['port'], '2222');
expect(formFields['user'], 'deploy');
expect(formFields['keyId'], '~/.ssh/production.pem');
});
test('import summary display logic', () {
// Test import summary formatting
const totalFound = 5;
const duplicatesRemoved = 2;
const serversToImport = 3;
final summary = {
'total': totalFound,
'duplicates': duplicatesRemoved,
'toImport': serversToImport,
};
expect(summary['total'], 5);
expect(summary['duplicates'], 2);
expect(summary['toImport'], 3);
// Summary validation
expect(summary['duplicates']! + summary['toImport']!, summary['total']);
// Format summary message (simplified)
final message = 'Found ${summary['total']} servers, '
'${summary['duplicates']} duplicates removed, '
'${summary['toImport']} will be imported.';
expect(message, 'Found 5 servers, 2 duplicates removed, 3 will be imported.');
});
test('error handling logic', () {
// Test error handling scenarios
final errors = <String>[];
// Validation errors
void validateServer(Spi server) {
if (server.name.isEmpty) {
errors.add('Server name is required');
}
if (server.ip.isEmpty) {
errors.add('Server IP is required');
}
if (server.port <= 0 || server.port > 65535) {
errors.add('Port must be between 1 and 65535');
}
if (server.user.isEmpty) {
errors.add('Username is required');
}
}
// Test with invalid server
const invalidServer = Spi(
name: '',
ip: '',
port: 0,
user: '',
);
validateServer(invalidServer);
expect(errors.length, 4);
expect(errors.contains('Server name is required'), isTrue);
expect(errors.contains('Server IP is required'), isTrue);
expect(errors.contains('Port must be between 1 and 65535'), isTrue);
expect(errors.contains('Username is required'), isTrue);
// Test with valid server
errors.clear();
const validServer = Spi(
name: 'valid',
ip: '192.168.1.1',
port: 22,
user: 'root',
);
validateServer(validServer);
expect(errors.isEmpty, isTrue);
});
test('name conflict resolution logic', () {
// Test name conflict resolution during import
final existingNames = ['server1', 'server2', 'server3'];
String resolveNameConflict(String proposedName, List<String> existing) {
if (!existing.contains(proposedName)) {
return proposedName;
}
int suffix = 2;
String newName;
do {
newName = '$proposedName ($suffix)';
suffix++;
} while (existing.contains(newName));
return newName;
}
// Test with no conflict
expect(resolveNameConflict('unique-name', existingNames), 'unique-name');
// Test with conflict
expect(resolveNameConflict('server1', existingNames), 'server1 (2)');
// Test with multiple conflicts
final extendedNames = [...existingNames, 'server1 (2)'];
expect(resolveNameConflict('server1', extendedNames), 'server1 (3)');
});
test('SSH config import button visibility logic', () {
// Test when SSH import button should be visible
bool shouldShowSSHImport({
required bool isDesktop,
required bool firstTimeReadSSHCfg,
required bool isNewServer,
}) {
return isDesktop && (firstTimeReadSSHCfg || !isNewServer);
}
// Desktop, first time, new server - should show
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: true,
isNewServer: true,
), isTrue);
// Desktop, not first time, new server - should not show auto import but manual import available
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: false,
isNewServer: true,
), isFalse);
// Desktop, editing existing server - should show manual import
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: false,
isNewServer: false,
), isTrue);
// Mobile - should never show
expect(shouldShowSSHImport(
isDesktop: false,
firstTimeReadSSHCfg: true,
isNewServer: true,
), isFalse);
});
});
}

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);
});
});
}