mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
feat: import servers from ~/.ssh/config (#873)
This commit is contained in:
414
test/server_dedup_test.dart
Normal file
414
test/server_dedup_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
317
test/server_edit_logic_test.dart
Normal file
317
test/server_edit_logic_test.dart
Normal 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
355
test/ssh_config_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user