From 63bf3241a191c37411c69dedca0bc9599bf51f2d Mon Sep 17 00:00:00 2001
From: Thomas Evensen <thomeven@gmail.com>
Date: Tue, 13 Aug 2024 19:19:37 +0200
Subject: [PATCH] Initial create

---
 Sources/SSHCreateKey/SSHCreateKey.swift       | 268 ++++++++++++++++++
 .../DecodeTestUserConfiguration.swift         |  66 +++++
 Tests/SSHCreateKeyTests/DecodeTestdata.swift  |  99 +++++++
 .../ReadTestdataFromGitHub.swift              |  40 +++
 .../SSHCreateKeyTests/SSHCreateKeyTests.swift |  25 +-
 .../TestSharedReference.swift                 |  67 +++++
 .../TestSynchronizeConfiguration.swift        | 140 +++++++++
 .../TestUserConfiguration.swift               | 172 +++++++++++
 .../TestdataFromGitHub.swift                  |  41 +++
 9 files changed, 916 insertions(+), 2 deletions(-)
 create mode 100644 Tests/SSHCreateKeyTests/DecodeTestUserConfiguration.swift
 create mode 100644 Tests/SSHCreateKeyTests/DecodeTestdata.swift
 create mode 100644 Tests/SSHCreateKeyTests/ReadTestdataFromGitHub.swift
 create mode 100644 Tests/SSHCreateKeyTests/TestSharedReference.swift
 create mode 100644 Tests/SSHCreateKeyTests/TestSynchronizeConfiguration.swift
 create mode 100644 Tests/SSHCreateKeyTests/TestUserConfiguration.swift
 create mode 100644 Tests/SSHCreateKeyTests/TestdataFromGitHub.swift

diff --git a/Sources/SSHCreateKey/SSHCreateKey.swift b/Sources/SSHCreateKey/SSHCreateKey.swift
index 08b22b8..12d44f9 100644
--- a/Sources/SSHCreateKey/SSHCreateKey.swift
+++ b/Sources/SSHCreateKey/SSHCreateKey.swift
@@ -1,2 +1,270 @@
 // The Swift Programming Language
 // https://docs.swift.org/swift-book
+import Foundation
+
+@MainActor
+public final class SSHCreateKey {
+    
+    // var offsiteServer = ""
+    // var offsiteUsername = ""
+
+    var sharedsshport: String?
+    var sharedsshkeypathandidentityfile: String?
+    
+    public var rsaStringPath: String?
+    // Arrays listing all key files
+    public var keyFileStrings: [String]? {
+        let fm = FileManager.default
+        if let atpath = keypathonly {
+            var array = [String]()
+            do {
+                for files in try fm.contentsOfDirectory(atPath: atpath) {
+                    array.append(files)
+                }
+                return array
+            } catch {
+                return nil
+            }
+        }
+        return nil
+    }
+
+    // Path to ssh keypath
+    public var sshkeypathandidentityfile: String? {
+        if let sharedsshkeypathandidentityfile,
+           let userHomeDirectoryPath
+        {
+            if sharedsshkeypathandidentityfile.first == "~" {
+                // must drop identityfile and then set rootpath
+                // also drop the "~" character
+                let sshkeypathandidentityfilesplit = sharedsshkeypathandidentityfile.split(separator: "/")
+                guard sshkeypathandidentityfilesplit.count > 2 else {
+                    // If anything goes wrong set to default global values
+                    return userHomeDirectoryPath + "/.ssh"
+                }
+                return userHomeDirectoryPath + sshkeypathandidentityfilesplit.joined(separator: "/").dropFirst()
+
+            } else {
+                // If anything goes wrong set to default global values
+                return userHomeDirectoryPath + "/.ssh"
+            }
+        } else {
+            return (userHomeDirectoryPath ?? "") + "/.ssh"
+        }
+    }
+
+    // SSH identityfile with full keypath if NOT default is used
+    // If default, only return defalt value
+    public var identityfile: String? {
+        if let sharedsshkeypathandidentityfile {
+            if sharedsshkeypathandidentityfile.first == "~" {
+                // must drop identityfile and then set rootpath
+                // also drop the "~" character
+                let sshkeypathandidentityfilesplit = sharedsshkeypathandidentityfile.split(separator: "/")
+                guard sshkeypathandidentityfilesplit.count > 2 else {
+                    // If anything goes wrong set to default global values
+                    return "id_rsa"
+                }
+                return String(sshkeypathandidentityfilesplit[sshkeypathandidentityfilesplit.count - 1])
+            } else {
+                // If anything goes wrong set to default global values
+                return "id_rsa"
+            }
+        } else {
+            return "id_rsa"
+        }
+    }
+
+    // Used when creating ssh keypath
+    public var keypathonly: String? {
+        if let sharedsshkeypathandidentityfile,
+           let userHomeDirectoryPath
+        {
+            if sharedsshkeypathandidentityfile.first == "~" {
+                // must drop identityfile and then set rootpath
+                // also drop the "~" character
+                var sshkeypathandidentityfilesplit = sharedsshkeypathandidentityfile.split(separator: "/")
+                guard sshkeypathandidentityfilesplit.count > 2 else {
+                    // If anything goes wrong set to default global values
+                    return NSHomeDirectory()
+                }
+                sshkeypathandidentityfilesplit.remove(at: sshkeypathandidentityfilesplit.count - 1)
+                return userHomeDirectoryPath +
+                String(sshkeypathandidentityfilesplit.joined(separator: "/").dropFirst())
+
+            } else {
+                // If anything goes wrong set to default global values
+                return userHomeDirectoryPath
+            }
+        } else {
+            return userHomeDirectoryPath
+        }
+    }
+
+    public var userHomeDirectoryPath: String? {
+        let pw = getpwuid(getuid())
+        if let home = pw?.pointee.pw_dir {
+            let homePath = FileManager.default.string(withFileSystemRepresentation: home, length: Int(strlen(home)))
+            return homePath
+        } else {
+            return nil
+        }
+    }
+
+    // Create SSH catalog
+    // If ssh catalog exists - bail out, no need to create
+    public func createsshkeyrootpath() {
+        let fm = FileManager.default
+        if let keypathonly,
+           let userHomeDirectoryPath
+        {
+            let sshkeypathString = userHomeDirectoryPath + "/." + keypathonly
+            guard fm.keypathlocationExists(at: sshkeypathString, kind: .folder) == false else {
+                return
+            }
+
+            let userHomeDirectoryPathURL = URL(fileURLWithPath: userHomeDirectoryPath)
+            let sshkeypathlURL = userHomeDirectoryPathURL.appendingPathComponent("/." + keypathonly)
+
+            do {
+                try fm.createDirectory(at: sshkeypathlURL, withIntermediateDirectories: true, attributes: nil)
+            } catch let e {
+                let error = e
+                // propogateerror(error: error)
+                return
+            }
+        }
+    }
+
+    // Set parameters for ssh-copy-id for copy public ssh key to server
+    // ssh-address = "backup@server.com"
+    // ssh-copy-id -i $ssh-keypath -p port $ssh-address
+    public func argumentssshcopyid(offsiteServer: String,
+                                   offsiteUsername: String) -> String {
+        var args = [String]()
+        let command = "/usr/bin/ssh-copy-id"
+        args.append(command)
+        args.append("-i")
+        if let sharedsshkeypathandidentityfile,
+           let sharedsshport,
+           sharedsshkeypathandidentityfile.isEmpty == false,
+           sharedsshport != "-1"
+        {
+            args.append(sharedsshkeypathandidentityfile)
+            args.append("-p")
+            args.append(sharedsshport)
+        }
+        args.append( offsiteUsername + "@" + offsiteServer)
+        return args.joined(separator: " ")
+    }
+
+    // Check if pub key exists on remote server
+    // ssh -p port -i $ssh-keypath $ssh-address
+    public func argumentscheckremotepubkey(offsiteServer: String,
+                                           offsiteUsername: String) -> String {
+        var args = [String]()
+        let command = "/usr/bin/ssh"
+        args.append(command)
+        if let sharedsshport, sharedsshport != "-1" {
+            args.append("-p")
+            args.append(sharedsshport)
+        }
+        args.append("-i")
+        if let sharedsshkeypathandidentityfile,
+           sharedsshkeypathandidentityfile.isEmpty == false
+        {
+            args.append(sharedsshkeypathandidentityfile)
+        }
+
+        args.append( offsiteUsername + "@" + offsiteServer)
+        return args.joined(separator: " ")
+    }
+
+    // Create local key with ssh-keygen
+    // Generate a passwordless RSA keyfile -N sets password, "" makes it blank
+    // ssh-keygen -t rsa -N "" -f $ssh-keypath
+    public func argumentscreatekey() -> [String]? {
+        var args = [String]()
+        args.append("-t")
+        args.append("rsa")
+        args.append("-N")
+        args.append("")
+        args.append("-f")
+        if let sharedsshkeypathandidentityfile,
+           sharedsshkeypathandidentityfile.isEmpty == false
+        {
+            args.append(sharedsshkeypathandidentityfile)
+        }
+        return args
+    }
+    
+    // Check if rsa pub key exists
+    public func islocalpublicrsakeypresent() throws -> Bool {
+        guard keyFileStrings != nil else { return false }
+        guard keyFileStrings?.filter({ $0.contains(identityfile ?? "") }).count ?? 0 > 0 else { return false }
+        guard keyFileStrings?.filter({ $0.contains((identityfile ?? "") + ".pub") }).count ?? 0 > 0 else {
+            throw SshError.sshkeys
+        }
+        rsaStringPath = keyFileStrings?.filter { $0.contains((identityfile ?? "") + ".pub") }[0]
+        guard rsaStringPath?.count ?? 0 > 0 else { return false }
+        throw SshError.sshkeys
+    }
+
+    public func validatepublickeypresent() -> Bool {
+        guard keyFileStrings != nil else { return false }
+        guard keyFileStrings?.filter({ $0.contains(identityfile ?? "") }).count ?? 0 > 0 else { return false }
+        guard keyFileStrings?.filter({ $0.contains((identityfile ?? "") + ".pub") }).count ?? 0 > 0 else {
+            return true
+        }
+        rsaStringPath = keyFileStrings?.filter { $0.contains((identityfile ?? "") + ".pub") }[0]
+        guard rsaStringPath?.count ?? 0 > 0 else { return false }
+        return true
+    }
+
+    public init( sharedsshport: String?,
+                sharedsshkeypathandidentityfile: String?)
+    {
+        self.sharedsshport = sharedsshport
+        self.sharedsshkeypathandidentityfile = sharedsshkeypathandidentityfile
+    }
+}
+
+extension FileManager {
+    func keypathlocationExists(at path: String, kind: LocationKind) -> Bool {
+        var isFolder: ObjCBool = false
+
+        guard fileExists(atPath: path, isDirectory: &isFolder) else {
+            return false
+        }
+
+        switch kind {
+        case .file: return !isFolder.boolValue
+        case .folder: return isFolder.boolValue
+        }
+    }
+}
+
+/// Enum describing various kinds of locations that can be found on a file system.
+public enum LocationKind {
+    /// A file can be found at the location.
+    case file
+    /// A folder can be found at the location.
+    case folder
+}
+
+public enum SshError: LocalizedError {
+    case notvalidpath
+    case sshkeys
+    case noslash
+
+    public var errorDescription: String? {
+        switch self {
+        case .notvalidpath:
+            "SSH keypath is not valid"
+        case .sshkeys:
+            "SSH RSA keys exist, cannot create"
+        case .noslash:
+            "SSH keypath must be like ~/.ssh_keypath/identityfile"
+        }
+    }
+}
diff --git a/Tests/SSHCreateKeyTests/DecodeTestUserConfiguration.swift b/Tests/SSHCreateKeyTests/DecodeTestUserConfiguration.swift
new file mode 100644
index 0000000..4511180
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/DecodeTestUserConfiguration.swift
@@ -0,0 +1,66 @@
+//
+//  DecodeTestUserConfiguration.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+struct DecodeTestUserConfiguration: Codable {
+    let rsyncversion3: Int?
+    // Detailed logging
+    let addsummarylogrecord: Int?
+    // Logging to logfile
+    let logtofile: Int?
+    // Monitor network connection
+    let monitornetworkconnection: Int?
+    // local path for rsync
+    let localrsyncpath: String?
+    // temporary path for restore
+    let pathforrestore: String?
+    // days for mark days since last synchronize
+    let marknumberofdayssince: String?
+    // Global ssh keypath and port
+    let sshkeypathandidentityfile: String?
+    let sshport: Int?
+    // Environment variable
+    let environment: String?
+    let environmentvalue: String?
+    let checkforerrorinrsyncoutput: Int?
+    // Confirm execute
+    let confirmexecute: Int?
+
+    enum CodingKeys: String, CodingKey {
+        case rsyncversion3
+        case addsummarylogrecord
+        case logtofile
+        case monitornetworkconnection
+        case localrsyncpath
+        case pathforrestore
+        case marknumberofdayssince
+        case sshkeypathandidentityfile
+        case sshport
+        case environment
+        case environmentvalue
+        case checkforerrorinrsyncoutput
+        case confirmexecute
+    }
+
+    init(from decoder: Decoder) throws {
+        let values = try decoder.container(keyedBy: CodingKeys.self)
+        rsyncversion3 = try values.decodeIfPresent(Int.self, forKey: .rsyncversion3)
+        addsummarylogrecord = try values.decodeIfPresent(Int.self, forKey: .addsummarylogrecord)
+        logtofile = try values.decodeIfPresent(Int.self, forKey: .logtofile)
+        monitornetworkconnection = try values.decodeIfPresent(Int.self, forKey: .monitornetworkconnection)
+        localrsyncpath = try values.decodeIfPresent(String.self, forKey: .localrsyncpath)
+        pathforrestore = try values.decodeIfPresent(String.self, forKey: .pathforrestore)
+        marknumberofdayssince = try values.decodeIfPresent(String.self, forKey: .marknumberofdayssince)
+        sshkeypathandidentityfile = try values.decodeIfPresent(String.self, forKey: .sshkeypathandidentityfile)
+        sshport = try values.decodeIfPresent(Int.self, forKey: .sshport)
+        environment = try values.decodeIfPresent(String.self, forKey: .environment)
+        environmentvalue = try values.decodeIfPresent(String.self, forKey: .environmentvalue)
+        checkforerrorinrsyncoutput = try values.decodeIfPresent(Int.self, forKey: .checkforerrorinrsyncoutput)
+        confirmexecute = try values.decodeIfPresent(Int.self, forKey: .confirmexecute)
+    }
+}
diff --git a/Tests/SSHCreateKeyTests/DecodeTestdata.swift b/Tests/SSHCreateKeyTests/DecodeTestdata.swift
new file mode 100644
index 0000000..2e2abb0
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/DecodeTestdata.swift
@@ -0,0 +1,99 @@
+//
+//  DecodeTestdata.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+struct DecodeTestdata: Codable {
+    let backupID: String?
+    let dateRun: String?
+    let hiddenID: Int?
+    let localCatalog: String?
+    let offsiteCatalog: String?
+    let offsiteServer: String?
+    let offsiteUsername: String?
+    let parameter1: String?
+    let parameter10: String?
+    let parameter11: String?
+    let parameter12: String?
+    let parameter13: String?
+    let parameter14: String?
+    let parameter2: String?
+    let parameter3: String?
+    let parameter4: String?
+    let parameter5: String?
+    let parameter6: String?
+    let parameter8: String?
+    let parameter9: String?
+    let rsyncdaemon: Int?
+    let sshkeypathandidentityfile: String?
+    let sshport: Int?
+    let task: String?
+    let snapdayoffweek: String?
+    let snaplast: Int?
+    let snapshotnum: Int?
+
+    enum CodingKeys: String, CodingKey {
+        case backupID
+        case dateRun
+        case hiddenID
+        case localCatalog
+        case offsiteCatalog
+        case offsiteServer
+        case offsiteUsername
+        case parameter1
+        case parameter10
+        case parameter11
+        case parameter12
+        case parameter13
+        case parameter14
+        case parameter2
+        case parameter3
+        case parameter4
+        case parameter5
+        case parameter6
+        case parameter8
+        case parameter9
+        case rsyncdaemon
+        case sshkeypathandidentityfile
+        case sshport
+        case task
+        case snapdayoffweek
+        case snaplast
+        case snapshotnum
+    }
+
+    init(from decoder: Decoder) throws {
+        let values = try decoder.container(keyedBy: CodingKeys.self)
+        backupID = try values.decodeIfPresent(String.self, forKey: .backupID)
+        dateRun = try values.decodeIfPresent(String.self, forKey: .dateRun)
+        hiddenID = try values.decodeIfPresent(Int.self, forKey: .hiddenID)
+        localCatalog = try values.decodeIfPresent(String.self, forKey: .localCatalog)
+        offsiteCatalog = try values.decodeIfPresent(String.self, forKey: .offsiteCatalog)
+        offsiteServer = try values.decodeIfPresent(String.self, forKey: .offsiteServer)
+        offsiteUsername = try values.decodeIfPresent(String.self, forKey: .offsiteUsername)
+        parameter1 = try values.decodeIfPresent(String.self, forKey: .parameter1)
+        parameter10 = try values.decodeIfPresent(String.self, forKey: .parameter10)
+        parameter11 = try values.decodeIfPresent(String.self, forKey: .parameter11)
+        parameter12 = try values.decodeIfPresent(String.self, forKey: .parameter12)
+        parameter13 = try values.decodeIfPresent(String.self, forKey: .parameter13)
+        parameter14 = try values.decodeIfPresent(String.self, forKey: .parameter14)
+        parameter2 = try values.decodeIfPresent(String.self, forKey: .parameter2)
+        parameter3 = try values.decodeIfPresent(String.self, forKey: .parameter3)
+        parameter4 = try values.decodeIfPresent(String.self, forKey: .parameter4)
+        parameter5 = try values.decodeIfPresent(String.self, forKey: .parameter5)
+        parameter6 = try values.decodeIfPresent(String.self, forKey: .parameter6)
+        parameter8 = try values.decodeIfPresent(String.self, forKey: .parameter8)
+        parameter9 = try values.decodeIfPresent(String.self, forKey: .parameter9)
+        rsyncdaemon = try values.decodeIfPresent(Int.self, forKey: .rsyncdaemon)
+        sshkeypathandidentityfile = try values.decodeIfPresent(String.self, forKey: .sshkeypathandidentityfile)
+        sshport = try values.decodeIfPresent(Int.self, forKey: .sshport)
+        task = try values.decodeIfPresent(String.self, forKey: .task)
+        snapdayoffweek = try values.decodeIfPresent(String.self, forKey: .snapdayoffweek)
+        snaplast = try values.decodeIfPresent(Int.self, forKey: .snaplast)
+        snapshotnum = try values.decodeIfPresent(Int.self, forKey: .snapshotnum)
+    }
+}
diff --git a/Tests/SSHCreateKeyTests/ReadTestdataFromGitHub.swift b/Tests/SSHCreateKeyTests/ReadTestdataFromGitHub.swift
new file mode 100644
index 0000000..99510ea
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/ReadTestdataFromGitHub.swift
@@ -0,0 +1,40 @@
+//
+//  ReadTestdataFromGitHub.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+final class ReadTestdataFromGitHub {
+    var testconfigurations = [TestSynchronizeConfiguration]()
+
+    func getdata() async {
+        let testdata = TestdataFromGitHub()
+        // Load user configuration
+        do {
+            if let userconfig = try await testdata.getrsyncuiconfigbyURL() {
+                await TestUserConfiguration(userconfig)
+                print("ReadTestdataFromGitHub: loading userconfiguration COMPLETED)")
+            }
+
+        } catch {
+            print("ReadTestdataFromGitHub: loading userconfiguration FAILED)")
+        }
+        // Load data
+        do {
+            if let testdata = try await testdata.gettestdatabyURL() {
+                testconfigurations.removeAll()
+                for i in 0 ..< testdata.count {
+                    var configuration = TestSynchronizeConfiguration(testdata[i])
+                    configuration.profile = "test"
+                    testconfigurations.append(configuration)
+                }
+                print("ReadTestdataFromGitHub: loading data COMPLETED)")
+            }
+        } catch {
+            print("ReadTestdataFromGitHub: loading data FAILED)")
+        }
+    }
+}
diff --git a/Tests/SSHCreateKeyTests/SSHCreateKeyTests.swift b/Tests/SSHCreateKeyTests/SSHCreateKeyTests.swift
index d3823af..adaa4b3 100644
--- a/Tests/SSHCreateKeyTests/SSHCreateKeyTests.swift
+++ b/Tests/SSHCreateKeyTests/SSHCreateKeyTests.swift
@@ -1,6 +1,27 @@
 import Testing
 @testable import SSHCreateKey
 
-@Test func example() async throws {
-    // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+@Suite final class TestCreateSSHkeys {
+    var testconfigurations: [TestSynchronizeConfiguration]?
+
+    @Test func LodaDataCreateSSHKeys() async {
+        let loadtestdata = ReadTestdataFromGitHub()
+        await loadtestdata.getdata()
+        testconfigurations = loadtestdata.testconfigurations
+        if let testconfigurations {
+            // Test for one configuration only, config nr 0
+            guard testconfigurations.count > 0 else { return }
+            let config = testconfigurations[0]
+            let createsshkeys = await SSHCreateKey(sharedsshport: String(TestSharedReference.shared.sshport ?? -1),
+                sharedsshkeypathandidentityfile: TestSharedReference.shared.sshkeypathandidentityfile
+            )
+
+            let arg3 = await createsshkeys.keypathonly
+            print(arg3 ?? "")
+            let arg4 = await createsshkeys.identityfile
+            print(arg4 ?? "")
+            let arg5 = await createsshkeys.userHomeDirectoryPath
+            print(arg5 ?? "")
+        }
+    }
 }
diff --git a/Tests/SSHCreateKeyTests/TestSharedReference.swift b/Tests/SSHCreateKeyTests/TestSharedReference.swift
new file mode 100644
index 0000000..7f4ded0
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/TestSharedReference.swift
@@ -0,0 +1,67 @@
+//
+//  TestSharedReference.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+@MainActor
+final class TestSharedReference {
+    @MainActor static let shared = TestSharedReference()
+    private init() {
+        synctasks = Set<String>()
+        synctasks = [synchronize, snapshot, syncremote]
+    }
+
+    var settingsischanged: Bool = false
+    var rsyncversion3: Bool = true
+    var localrsyncpath: String?
+    var norsync: Bool = false
+    var pathforrestore: String?
+    var addsummarylogrecord: Bool = true
+    var logtofile: Bool = false
+    var marknumberofdayssince: Int = 5
+    var environment: String?
+    var environmentvalue: String?
+    var sshport: Int?
+    var sshkeypathandidentityfile: String?
+    var checkforerrorinrsyncoutput: Bool = false
+    var monitornetworkconnection: Bool = false
+    var confirmexecute: Bool = false
+    var URLnewVersion: String?
+    let rsync: String = "rsync"
+    let usrbin: String = "/usr/bin"
+    let usrlocalbin: String = "/usr/local/bin"
+    let usrlocalbinarm: String = "/opt/homebrew/bin"
+    var macosarm: Bool = true
+    // RsyncUI config files and path
+    let configpath: String = "/.rsyncosx/"
+    let logname: String = "rsyncui.txt"
+    // Userconfiguration json file
+    let userconfigjson: String = "rsyncuiconfig.json"
+    // String tasks
+    let synchronize: String = "synchronize"
+    let snapshot: String = "snapshot"
+    let syncremote: String = "syncremote"
+    var synctasks: Set<String>
+    // rsync short version
+    var rsyncversionshort: String?
+    // filsize logfile warning
+    let logfilesize: Int = 100_000
+    // Mac serialnumer
+    var macserialnumber: String?
+    // True if menuapp is running
+    // var menuappisrunning: Bool = false
+    // Reference to the active process
+    var process: Process?
+    // JSON names
+    let filenamelogrecordsjson = "logrecords.json"
+    let fileconfigurationsjson = "configurations.json"
+    // Object for propogate errors to views
+    // var errorobject: AlertError?
+    // Used when starting up RsyncUI
+    // Default profile
+    let defaultprofile = "Testprofile"
+}
diff --git a/Tests/SSHCreateKeyTests/TestSynchronizeConfiguration.swift b/Tests/SSHCreateKeyTests/TestSynchronizeConfiguration.swift
new file mode 100644
index 0000000..9ef8a49
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/TestSynchronizeConfiguration.swift
@@ -0,0 +1,140 @@
+//
+//  TestSynchronizeConfiguration.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+struct TestSynchronizeConfiguration: Identifiable, Codable {
+    var id = UUID()
+    var hiddenID: Int
+    var task: String
+    var localCatalog: String
+    var offsiteCatalog: String
+    var offsiteUsername: String
+    var parameter1: String
+    var parameter2: String
+    var parameter3: String
+    var parameter4: String
+    var parameter5: String
+    var parameter6: String
+    var offsiteServer: String
+    var backupID: String
+    var dateRun: String?
+    var snapshotnum: Int?
+    // parameters choosed by user
+    var parameter8: String?
+    var parameter9: String?
+    var parameter10: String?
+    var parameter11: String?
+    var parameter12: String?
+    var parameter13: String?
+    var parameter14: String?
+    var rsyncdaemon: Int?
+    // SSH parameters
+    var sshport: Int?
+    var sshkeypathandidentityfile: String?
+
+    var profile: String?
+    // Snapshots, day to save and last = 1 or every last=0
+    var snapdayoffweek: String?
+    var snaplast: Int?
+
+    // Used when reading JSON data from store
+    // see in ReadConfigurationJSON
+    init(_ data: DecodeTestdata) {
+        backupID = data.backupID ?? ""
+        hiddenID = data.hiddenID ?? -1
+        localCatalog = data.localCatalog ?? ""
+        offsiteCatalog = data.offsiteCatalog ?? ""
+        offsiteServer = data.offsiteServer ?? ""
+        offsiteUsername = data.offsiteUsername ?? ""
+        parameter1 = data.parameter1 ?? ""
+        parameter10 = data.parameter10
+        parameter11 = data.parameter11
+        parameter12 = data.parameter12
+        parameter13 = data.parameter13
+        parameter14 = data.parameter14
+        parameter2 = data.parameter2 ?? ""
+        parameter3 = data.parameter3 ?? ""
+        parameter4 = data.parameter4 ?? ""
+        parameter5 = data.parameter5 ?? ""
+        parameter6 = data.parameter6 ?? ""
+        parameter8 = data.parameter8
+        parameter9 = data.parameter9
+        rsyncdaemon = data.rsyncdaemon
+        sshkeypathandidentityfile = data.sshkeypathandidentityfile
+        sshport = data.sshport ?? -1
+        task = data.task ?? ""
+        // For snapshots
+        if let snapshotnum = data.snapshotnum {
+            self.snapshotnum = snapshotnum
+            snapdayoffweek = data.snapdayoffweek ?? TestStringDayofweek.Sunday.rawValue
+            snaplast = data.snaplast ?? 1
+        }
+        // Last run of task
+        dateRun = data.dateRun
+    }
+}
+
+extension TestSynchronizeConfiguration: Hashable, Equatable {
+    static func == (lhs: TestSynchronizeConfiguration, rhs: TestSynchronizeConfiguration) -> Bool {
+        lhs.localCatalog == rhs.localCatalog &&
+            lhs.offsiteCatalog == rhs.offsiteCatalog &&
+            lhs.offsiteUsername == rhs.offsiteUsername &&
+            lhs.offsiteServer == rhs.offsiteServer &&
+            lhs.hiddenID == rhs.hiddenID &&
+            lhs.task == rhs.task &&
+            lhs.parameter1 == rhs.parameter1 &&
+            lhs.parameter2 == rhs.parameter2 &&
+            lhs.parameter3 == rhs.parameter3 &&
+            lhs.parameter4 == rhs.parameter4 &&
+            lhs.parameter5 == rhs.parameter5 &&
+            lhs.parameter6 == rhs.parameter6 &&
+            lhs.parameter8 == rhs.parameter8 &&
+            lhs.parameter9 == rhs.parameter9 &&
+            lhs.parameter10 == rhs.parameter10 &&
+            lhs.parameter11 == rhs.parameter11 &&
+            lhs.parameter12 == rhs.parameter12 &&
+            lhs.parameter13 == rhs.parameter13 &&
+            lhs.parameter14 == rhs.parameter14 &&
+            lhs.dateRun == rhs.dateRun
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(localCatalog)
+        hasher.combine(offsiteUsername)
+        hasher.combine(offsiteServer)
+        hasher.combine(String(hiddenID))
+        hasher.combine(task)
+        hasher.combine(parameter1)
+        hasher.combine(parameter2)
+        hasher.combine(parameter3)
+        hasher.combine(parameter4)
+        hasher.combine(parameter5)
+        hasher.combine(parameter6)
+        hasher.combine(parameter8)
+        hasher.combine(parameter9)
+        hasher.combine(parameter10)
+        hasher.combine(parameter11)
+        hasher.combine(parameter12)
+        hasher.combine(parameter13)
+        hasher.combine(parameter14)
+        hasher.combine(dateRun)
+    }
+}
+
+enum TestStringDayofweek: String, CaseIterable, Identifiable, CustomStringConvertible {
+    case Monday
+    case Tuesday
+    case Wednesday
+    case Thursday
+    case Friday
+    case Saturday
+    case Sunday
+
+    var id: String { rawValue }
+    var description: String { rawValue.localizedLowercase }
+}
diff --git a/Tests/SSHCreateKeyTests/TestUserConfiguration.swift b/Tests/SSHCreateKeyTests/TestUserConfiguration.swift
new file mode 100644
index 0000000..85cb981
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/TestUserConfiguration.swift
@@ -0,0 +1,172 @@
+//
+//  TestUserConfiguration.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+struct TestUserConfiguration: Codable {
+    var rsyncversion3: Int = -1
+    // Detailed logging
+    var addsummarylogrecord: Int = 1
+    // Logging to logfile
+    var logtofile: Int = 1
+    // Montor network connection
+    var monitornetworkconnection: Int = -1
+    // local path for rsync
+    var localrsyncpath: String?
+    // temporary path for restore
+    var pathforrestore: String?
+    // days for mark days since last synchronize
+    var marknumberofdayssince: String = "5"
+    // Global ssh keypath and port
+    var sshkeypathandidentityfile: String?
+    var sshport: Int?
+    // Environment variable
+    var environment: String?
+    var environmentvalue: String?
+    // Check for error in output from rsync
+    var checkforerrorinrsyncoutput: Int = -1
+    // Automatic execution
+    var confirmexecute: Int?
+
+    @MainActor private func setuserconfigdata() {
+        if rsyncversion3 == 1 {
+            TestSharedReference.shared.rsyncversion3 = true
+        } else {
+            TestSharedReference.shared.rsyncversion3 = false
+        }
+        if addsummarylogrecord == 1 {
+            TestSharedReference.shared.addsummarylogrecord = true
+        } else {
+            TestSharedReference.shared.addsummarylogrecord = false
+        }
+        if logtofile == 1 {
+            TestSharedReference.shared.logtofile = true
+        } else {
+            TestSharedReference.shared.logtofile = false
+        }
+        if monitornetworkconnection == 1 {
+            TestSharedReference.shared.monitornetworkconnection = true
+        } else {
+            TestSharedReference.shared.monitornetworkconnection = false
+        }
+        if localrsyncpath != nil {
+            TestSharedReference.shared.localrsyncpath = localrsyncpath
+        } else {
+            TestSharedReference.shared.localrsyncpath = nil
+        }
+        if pathforrestore != nil {
+            TestSharedReference.shared.pathforrestore = pathforrestore
+        } else {
+            TestSharedReference.shared.pathforrestore = nil
+        }
+        if Int(marknumberofdayssince) ?? 0 > 0 {
+            TestSharedReference.shared.marknumberofdayssince = Int(marknumberofdayssince) ?? 0
+        }
+        if sshkeypathandidentityfile != nil {
+            TestSharedReference.shared.sshkeypathandidentityfile = sshkeypathandidentityfile
+        }
+        if sshport != nil {
+            TestSharedReference.shared.sshport = sshport
+        }
+        if environment != nil {
+            TestSharedReference.shared.environment = environment
+        }
+        if environmentvalue != nil {
+            TestSharedReference.shared.environmentvalue = environmentvalue
+        }
+        if checkforerrorinrsyncoutput == 1 {
+            TestSharedReference.shared.checkforerrorinrsyncoutput = true
+        } else {
+            TestSharedReference.shared.checkforerrorinrsyncoutput = false
+        }
+        if confirmexecute == 1 {
+            TestSharedReference.shared.confirmexecute = true
+        } else {
+            TestSharedReference.shared.confirmexecute = false
+        }
+    }
+
+    // Used when reading JSON data from store
+    @discardableResult
+    @MainActor init(_ data: DecodeTestUserConfiguration) {
+        rsyncversion3 = data.rsyncversion3 ?? -1
+        addsummarylogrecord = data.addsummarylogrecord ?? 1
+        logtofile = data.logtofile ?? 0
+        monitornetworkconnection = data.monitornetworkconnection ?? -1
+        localrsyncpath = data.localrsyncpath
+        pathforrestore = data.pathforrestore
+        marknumberofdayssince = data.marknumberofdayssince ?? "5"
+        sshkeypathandidentityfile = data.sshkeypathandidentityfile
+        sshport = data.sshport
+        environment = data.environment
+        environmentvalue = data.environmentvalue
+        checkforerrorinrsyncoutput = data.checkforerrorinrsyncoutput ?? -1
+        confirmexecute = data.confirmexecute ?? -1
+        // Set user configdata read from permanent store
+        setuserconfigdata()
+    }
+
+    // Default values user configuration
+    @discardableResult
+    @MainActor init() {
+        if TestSharedReference.shared.rsyncversion3 {
+            rsyncversion3 = 1
+        } else {
+            rsyncversion3 = -1
+        }
+        if TestSharedReference.shared.addsummarylogrecord {
+            addsummarylogrecord = 1
+        } else {
+            addsummarylogrecord = -1
+        }
+        if TestSharedReference.shared.logtofile {
+            logtofile = 1
+        } else {
+            logtofile = -1
+        }
+        if TestSharedReference.shared.monitornetworkconnection {
+            monitornetworkconnection = 1
+        } else {
+            monitornetworkconnection = -1
+        }
+        if TestSharedReference.shared.localrsyncpath != nil {
+            localrsyncpath = TestSharedReference.shared.localrsyncpath
+        } else {
+            localrsyncpath = nil
+        }
+        if TestSharedReference.shared.pathforrestore != nil {
+            pathforrestore = TestSharedReference.shared.pathforrestore
+        } else {
+            pathforrestore = nil
+        }
+        marknumberofdayssince = String(TestSharedReference.shared.marknumberofdayssince)
+        if TestSharedReference.shared.sshkeypathandidentityfile != nil {
+            sshkeypathandidentityfile = TestSharedReference.shared.sshkeypathandidentityfile
+        }
+        if TestSharedReference.shared.sshport != nil {
+            sshport = TestSharedReference.shared.sshport
+        }
+        if TestSharedReference.shared.environment != nil {
+            environment = TestSharedReference.shared.environment
+        }
+        if TestSharedReference.shared.environmentvalue != nil {
+            environmentvalue = TestSharedReference.shared.environmentvalue
+        }
+        if TestSharedReference.shared.checkforerrorinrsyncoutput == true {
+            checkforerrorinrsyncoutput = 1
+        } else {
+            checkforerrorinrsyncoutput = -1
+        }
+        if TestSharedReference.shared.confirmexecute == true {
+            confirmexecute = 1
+        } else {
+            confirmexecute = -1
+        }
+    }
+}
+
+// swiftlint:enable cyclomatic_complexity function_body_length
diff --git a/Tests/SSHCreateKeyTests/TestdataFromGitHub.swift b/Tests/SSHCreateKeyTests/TestdataFromGitHub.swift
new file mode 100644
index 0000000..9b4696f
--- /dev/null
+++ b/Tests/SSHCreateKeyTests/TestdataFromGitHub.swift
@@ -0,0 +1,41 @@
+//
+//  TestdataFromGitHub.swift
+//  RsyncArguments
+//
+//  Created by Thomas Evensen on 05/08/2024.
+//
+
+import Foundation
+
+struct TestdataFromGitHub {
+    let urlSession = URLSession.shared
+    let jsonDecoder = JSONDecoder()
+
+    private var urlJSON: String = "https://raw.githubusercontent.com/rsyncOSX/RsyncArguments/master/Testdata/configurations.json"
+
+    func gettestdatabyURL() async throws -> [DecodeTestdata]? {
+        if let url = URL(string: urlJSON) {
+            let (data, _) = try await urlSession.gettestdata(for: url)
+            return try jsonDecoder.decode([DecodeTestdata].self, from: data)
+        } else {
+            return nil
+        }
+    }
+
+    private var urlJSONuiconfig: String = "https://raw.githubusercontent.com/rsyncOSX/RsyncArguments/master/Testdata/rsyncuiconfig.json"
+
+    func getrsyncuiconfigbyURL() async throws -> DecodeTestUserConfiguration? {
+        if let url = URL(string: urlJSONuiconfig) {
+            let (data, _) = try await urlSession.gettestdata(for: url)
+            return try jsonDecoder.decode(DecodeTestUserConfiguration.self, from: data)
+        } else {
+            return nil
+        }
+    }
+}
+
+public extension URLSession {
+    func gettestdata(for url: URL) async throws -> (Data, URLResponse) {
+        try await data(from: url)
+    }
+}
-- 
GitLab