295 lines
11 KiB
Swift
295 lines
11 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AppKit
|
|
import Foundation
|
|
import SwiftData
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import TermindPresentation
|
|
import TermindRemoteInfrastructure
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@testable import Termind
|
|
|
|
@Suite
|
|
@MainActor
|
|
struct ConnectionHistoryTests {
|
|
@Test
|
|
func desktopHistoryProjectionTruncatesItsInput() {
|
|
let input = Array(0...100)
|
|
let projected = DesktopHistoryPresentation.limited(input)
|
|
#expect(projected == Array(0..<100))
|
|
}
|
|
|
|
@Test
|
|
func schemaV5MigratesWithAnEmptyConnectionTimeline() throws {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("ConnectionHistoryMigration-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(
|
|
at: directory, withIntermediateDirectories: true
|
|
)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let storeURL = directory.appendingPathComponent("Termind.store")
|
|
let legacySchema = Schema(versionedSchema: TermindSchemaV5.self)
|
|
|
|
var legacyContainer: ModelContainer? = try ModelContainer(
|
|
for: legacySchema,
|
|
configurations: [
|
|
localModelConfiguration(
|
|
"ConnectionHistoryMigration", schema: legacySchema, url: storeURL
|
|
)
|
|
]
|
|
)
|
|
let legacyHost = TermindSchemaV7HostModels.PersistentHost(
|
|
Host(name: "Existing", host: "existing.example")
|
|
)
|
|
legacyHost.lastUsedAt = Date(timeIntervalSince1970: 123)
|
|
legacyContainer?.mainContext.insert(legacyHost)
|
|
try legacyContainer?.mainContext.save()
|
|
legacyContainer = nil
|
|
|
|
let migrated = try ModelContainer(
|
|
for: HostPersistence.schema,
|
|
migrationPlan: TermindMigrationPlan.self,
|
|
configurations: [
|
|
localModelConfiguration(
|
|
"ConnectionHistoryMigration",
|
|
schema: HostPersistence.schema, url: storeURL
|
|
)
|
|
]
|
|
)
|
|
|
|
let migratedHosts = try migrated.mainContext.fetch(FetchDescriptor<PersistentHost>())
|
|
#expect(migratedHosts.count == 1)
|
|
#expect(
|
|
try migrated.mainContext.fetch(
|
|
FetchDescriptor<PersistentConnectionHistoryEntry>()
|
|
).isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func repeatedHostsRemainDistinctAndSortedByAttemptTime() {
|
|
let hostID = UUID()
|
|
let model = ConnectionHistoryModel()
|
|
|
|
model.record(kind: .ssh, for: hostID, at: Date(timeIntervalSince1970: 1))
|
|
model.record(kind: .sftp, for: hostID, at: Date(timeIntervalSince1970: 3))
|
|
model.record(kind: .ssh, for: hostID, at: Date(timeIntervalSince1970: 2))
|
|
|
|
let entries = model.entries()
|
|
#expect(entries.map(\.kind) == [.sftp, .ssh, .ssh])
|
|
#expect(
|
|
entries.map(\.connectedAt) == [
|
|
Date(timeIntervalSince1970: 3),
|
|
Date(timeIntervalSince1970: 2),
|
|
Date(timeIntervalSince1970: 1),
|
|
])
|
|
#expect(Set(entries.map(\.id)).count == 3)
|
|
}
|
|
|
|
@Test
|
|
func recordsTheLocalDeviceAndEndsWhenItsPaneCloses() throws {
|
|
let host = Host(name: "Server", host: "server.example")
|
|
let deviceID = UUID()
|
|
let devices = DeviceDirectoryModel(
|
|
currentDeviceID: deviceID, currentDeviceName: "Test Mac")
|
|
let history = ConnectionHistoryModel(sourceDeviceID: deviceID)
|
|
let app = AppModel(
|
|
hosts: HostLibraryModel(hosts: [host]), connectionHistory: history,
|
|
devices: devices)
|
|
let window = WindowModel()
|
|
|
|
let workspace = try #require(app.open(host, as: .sshSession, in: window))
|
|
let opened = try #require(history.entries().first)
|
|
#expect(opened.sourceDeviceID == deviceID)
|
|
#expect(devices.name(for: opened.sourceDeviceID) == "Test Mac")
|
|
#expect(opened.endedAt == nil)
|
|
|
|
window.close(workspace.id)
|
|
|
|
let ended = try #require(history.entries().first)
|
|
#expect(ended.endedAt != nil)
|
|
#expect(ended.endedAt.map { $0 >= ended.connectedAt } == true)
|
|
}
|
|
|
|
@Test
|
|
func localProjectionUsesStableDeviceIDAndDeviceRenameUpdatesExistingEvents() throws {
|
|
let localID = UUID()
|
|
let remoteID = UUID()
|
|
let hostID = UUID()
|
|
let stableAfterRename = ConnectionHistoryEntry(
|
|
hostID: hostID, kind: .ssh, connectedAt: Date(timeIntervalSince1970: 3),
|
|
sourceDeviceID: localID)
|
|
let missingDeviceID = ConnectionHistoryEntry(
|
|
hostID: hostID, kind: .telnet, connectedAt: Date(timeIntervalSince1970: 2),
|
|
sourceDeviceID: nil)
|
|
let remote = ConnectionHistoryEntry(
|
|
hostID: hostID, kind: .sftp, connectedAt: Date(timeIntervalSince1970: 1),
|
|
sourceDeviceID: remoteID)
|
|
let historyRepository = InMemoryConnectionHistoryRepository(
|
|
entries: [stableAfterRename, missingDeviceID, remote])
|
|
let model = ConnectionHistoryModel(
|
|
repository: historyRepository, sourceDeviceID: localID)
|
|
let devices = DeviceDirectoryModel(
|
|
currentDeviceID: localID, currentDeviceName: "old-name")
|
|
|
|
#expect(model.localEntries().map(\.id) == [stableAfterRename.id])
|
|
#expect(model.isLocal(stableAfterRename))
|
|
#expect(!model.isLocal(missingDeviceID))
|
|
#expect(!model.isLocal(remote))
|
|
#expect(devices.name(for: stableAfterRename.sourceDeviceID) == "old-name")
|
|
|
|
devices.updateCurrentDeviceName("Renamed Test Mac")
|
|
#expect(devices.name(for: stableAfterRename.sourceDeviceID) == "Renamed Test Mac")
|
|
}
|
|
|
|
@Test
|
|
func globalLimitKeepsTheNewestAttempts() {
|
|
let model = ConnectionHistoryModel()
|
|
for index in 0...250 {
|
|
model.record(
|
|
kind: .ssh, for: UUID(),
|
|
at: Date(timeIntervalSince1970: TimeInterval(index))
|
|
)
|
|
}
|
|
|
|
let entries = model.entries()
|
|
#expect(entries.count == 250)
|
|
#expect(
|
|
entries.first?.connectedAt
|
|
== Date(timeIntervalSince1970: 250))
|
|
#expect(entries.last?.connectedAt == Date(timeIntervalSince1970: 1))
|
|
}
|
|
|
|
@Test
|
|
func loadedConnectionHistoryImmediatelyTrimsToTheSharedLimit() {
|
|
let model = ConnectionHistoryModel(
|
|
entries: (0...250).map { index in
|
|
ConnectionHistoryEntry(
|
|
hostID: UUID(), kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: TimeInterval(index))
|
|
)
|
|
})
|
|
|
|
let entries = model.entries()
|
|
#expect(entries.count == 250)
|
|
#expect(
|
|
entries.first?.connectedAt
|
|
== Date(timeIntervalSince1970: 250))
|
|
#expect(entries.last?.connectedAt == Date(timeIntervalSince1970: 1))
|
|
}
|
|
|
|
@Test
|
|
func swiftDataRepositoryPersistsDeletesAndClears() throws {
|
|
let configuration = localModelConfiguration(
|
|
"ConnectionHistoryTests", schema: HostPersistence.schema,
|
|
isStoredInMemoryOnly: true
|
|
)
|
|
let container = try ModelContainer(
|
|
for: HostPersistence.schema, configurations: [configuration]
|
|
)
|
|
let hostID = UUID()
|
|
let otherHostID = UUID()
|
|
let model = ConnectionHistoryModel(modelContext: container.mainContext)
|
|
|
|
let first = try #require(model.record(kind: .ssh, for: hostID))
|
|
model.record(kind: .sftp, for: hostID)
|
|
model.record(kind: .ssh, for: otherHostID)
|
|
#expect(model.storageError == nil)
|
|
#expect(
|
|
try container.mainContext.fetch(
|
|
FetchDescriptor<PersistentConnectionHistoryEntry>()
|
|
).count == 3)
|
|
|
|
model.delete(first.id)
|
|
#expect(model.entries(for: hostID).count == 1)
|
|
model.clear(for: hostID)
|
|
#expect(model.entries(for: hostID).isEmpty)
|
|
#expect(model.entries(for: otherHostID).count == 1)
|
|
model.clearAll()
|
|
#expect(model.entries().isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func openingSavedHostsRecordsKindsAndDrivesRecents() {
|
|
let first = Host(name: "First", host: "first.example")
|
|
let second = Host(name: "Second", host: "second.example")
|
|
let history = ConnectionHistoryModel()
|
|
let app = AppModel(
|
|
hosts: HostLibraryModel(hosts: [first, second]),
|
|
connectionHistory: history
|
|
)
|
|
let window = WindowModel()
|
|
|
|
app.open(first, as: .sshSession, in: window)
|
|
app.open(second, as: .sftpChannel, in: window)
|
|
app.open(first, as: .sftpChannel, in: window)
|
|
|
|
#expect(history.entries().map(\.hostID) == [first.id, second.id, first.id])
|
|
#expect(history.entries().map(\.kind) == [.sftp, .sftp, .ssh])
|
|
#expect(history.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [first.id, second.id])
|
|
#expect(app.lastConnectedAt(for: first.id) != nil)
|
|
}
|
|
|
|
@Test
|
|
func welcomeRecentsUseOnlyThisDevicesLatestEvents() {
|
|
let localID = UUID()
|
|
let local = Host(name: "Local", host: "local.example")
|
|
let remote = Host(name: "Remote", host: "remote.example")
|
|
let history = ConnectionHistoryModel(
|
|
entries: [
|
|
ConnectionHistoryEntry(
|
|
hostID: remote.id, kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: 20),
|
|
sourceDeviceID: UUID()),
|
|
ConnectionHistoryEntry(
|
|
hostID: local.id, kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: 10),
|
|
sourceDeviceID: localID),
|
|
],
|
|
sourceDeviceID: localID)
|
|
let app = AppModel(
|
|
hosts: HostLibraryModel(hosts: [local, remote]), connectionHistory: history)
|
|
|
|
#expect(history.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [local.id])
|
|
#expect(history.entries().map(\.hostID) == [remote.id, local.id])
|
|
#expect(app.lastConnectedLocallyAt(for: local.id) == Date(timeIntervalSince1970: 10))
|
|
#expect(app.lastConnectedLocallyAt(for: remote.id) == nil)
|
|
}
|
|
|
|
@Test
|
|
func clearAllHistoryForHostKeepsHostAndOtherHostHistory() {
|
|
let host = Host(name: "Host", host: "host.example")
|
|
let other = Host(name: "Other", host: "other.example")
|
|
let localID = UUID()
|
|
let connections = ConnectionHistoryModel(
|
|
entries: [
|
|
ConnectionHistoryEntry(hostID: host.id, kind: .ssh, sourceDeviceID: localID),
|
|
ConnectionHistoryEntry(hostID: other.id, kind: .ssh, sourceDeviceID: localID),
|
|
],
|
|
sourceDeviceID: localID)
|
|
let session = SSHSessionModel(host: host)
|
|
session.commandHistory.record("uptime")
|
|
let files = SFTPHistoryModel()
|
|
files.record(kind: .directory, path: "/srv")
|
|
let app = AppModel(
|
|
hosts: HostLibraryModel(hosts: [host, other]),
|
|
connectionHistory: connections
|
|
)
|
|
|
|
app.clearAllHistory(for: [host.id])
|
|
|
|
#expect(app.hostLibrary.host(host.id) != nil)
|
|
#expect(connections.entries(for: host.id).isEmpty)
|
|
#expect(session.commandHistory.entries().map(\.command) == ["uptime"])
|
|
#expect(files.entries().map(\.path) == ["/srv"])
|
|
#expect(connections.entries(for: other.id).count == 1)
|
|
#expect(connections.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [other.id])
|
|
}
|
|
}
|