diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 9ecb95e..6a26723 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -9,20 +9,21 @@ /* Begin PBXBuildFile section */ 961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; }; 9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 961679532CFF207900B2B6DF /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 961679522CFF207900B2B6DF /* SwiftProtobuf */; }; - 961679552CFF207900B2B6DF /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */; }; AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; - AA3B3DB42D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; - AA3B3DB52D2D23860099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AA3B3DBF2D2D23AB0099996A /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */; }; AA3B3DC12D2D23AB0099996A /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */; }; AA3B3DCD2D2D249F0099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AA3B3E8E2D2E0FF40099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3E8D2D2E0FF40099996A /* Mocker */; }; + AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; }; + AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; }; + AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40B52D2FD9DD0099996A /* Mocker */; }; + AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; }; + AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40BC2D2FDFBA0099996A /* Mocker */; }; + AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; }; AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; }; AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; }; AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; }; - AAD720D02D0816B200F6304D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = AAD720CF2D0816B200F6304D /* Alamofire */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,19 +62,47 @@ remoteGlobalIDString = 961678FB2CFF100D00B2B6DF; remoteInfo = "Coder Desktop"; }; - AA3B3DB22D2D23860099996A /* PBXContainerItemProxy */ = { + AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; proxyType = 1; remoteGlobalIDString = AA3B3DA02D2D23860099996A; remoteInfo = VPNLib; }; - AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */ = { + AA3B409A2D2FC8560099996A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; proxyType = 1; - remoteGlobalIDString = AA3B3DA02D2D23860099996A; - remoteInfo = VPNLib; + remoteGlobalIDString = AA3B40902D2FC8560099996A; + remoteInfo = CoderSDK; + }; + AA3B409C2D2FC8560099996A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 961678FB2CFF100D00B2B6DF; + remoteInfo = "Coder Desktop"; + }; + AA3B40A22D2FC8560099996A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA3B40902D2FC8560099996A; + remoteInfo = CoderSDK; + }; + AA3B40B92D2FDA5C0099996A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA3B40902D2FC8560099996A; + remoteInfo = CoderSDK; + }; + AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA3B40902D2FC8560099996A; + remoteInfo = CoderSDK; }; /* End PBXContainerItemProxy section */ @@ -100,17 +129,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - AA3B3D922D2D233E0099996A /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - AA3B3DB52D2D23860099996A /* VPNLib.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -121,6 +139,8 @@ 961679322CFF117300B2B6DF /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; AA3B3DA12D2D23860099996A /* VPNLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VPNLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VPNLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3B40912D2FC8560099996A /* CoderSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoderSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoderSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -134,6 +154,13 @@ ); target = AA3B3DA02D2D23860099996A /* VPNLib */; }; + AA3B40A62D2FC8560099996A /* Exceptions for "CoderSDK" folder in "CoderSDK" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + publicHeaders = ( + CoderSDK.h, + ); + target = AA3B40902D2FC8560099996A /* CoderSDK */; + }; AA3C69C12D2D15D200A45481 /* Exceptions for "VPN" folder in "VPN" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -172,6 +199,19 @@ path = VPNLibTests; sourceTree = "<group>"; }; + AA3B40922D2FC8560099996A /* CoderSDK */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + AA3B40A62D2FC8560099996A /* Exceptions for "CoderSDK" folder in "CoderSDK" target */, + ); + path = CoderSDK; + sourceTree = "<group>"; + }; + AA3B409E2D2FC8560099996A /* CoderSDKTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CoderSDKTests; + sourceTree = "<group>"; + }; AA3C69AD2D2D143400A45481 /* VPN */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -187,12 +227,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AAD720D02D0816B200F6304D /* Alamofire in Frameworks */, - AA3B3DB42D2D23860099996A /* VPNLib.framework in Frameworks */, + AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */, AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */, AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */, - 961679552CFF207900B2B6DF /* SwiftProtobufPluginLibrary in Frameworks */, - 961679532CFF207900B2B6DF /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -201,6 +238,8 @@ buildActionMask = 2147483647; files = ( AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */, + AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */, + AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -225,6 +264,7 @@ buildActionMask = 2147483647; files = ( AA3B3DC12D2D23AB0099996A /* SwiftProtobufPluginLibrary in Frameworks */, + AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */, AA3B3DBF2D2D23AB0099996A /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -238,6 +278,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA3B408E2D2FC8560099996A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3B40952D2FC8560099996A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */, + AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -250,6 +306,8 @@ 9616791C2CFF100E00B2B6DF /* Coder DesktopUITests */, AA3B3DA22D2D23860099996A /* VPNLib */, AA3B3DAE2D2D23860099996A /* VPNLibTests */, + AA3B40922D2FC8560099996A /* CoderSDK */, + AA3B409E2D2FC8560099996A /* CoderSDKTests */, 961679312CFF117300B2B6DF /* Frameworks */, 961678FD2CFF100D00B2B6DF /* Products */, ); @@ -264,6 +322,8 @@ 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */, AA3B3DA12D2D23860099996A /* VPNLib.framework */, AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */, + AA3B40912D2FC8560099996A /* CoderSDK.framework */, + AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */, ); name = Products; sourceTree = "<group>"; @@ -286,6 +346,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA3B408C2D2FC8560099996A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -297,14 +364,13 @@ 961678F92CFF100D00B2B6DF /* Frameworks */, 961678FA2CFF100D00B2B6DF /* Resources */, 961679422CFF117300B2B6DF /* Embed System Extensions */, - AA3B3D922D2D233E0099996A /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */, 9616793C2CFF117300B2B6DF /* PBXTargetDependency */, - AA3B3DB32D2D23860099996A /* PBXTargetDependency */, + AA3B40A32D2FC8560099996A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 961678FE2CFF100D00B2B6DF /* Coder Desktop */, @@ -313,9 +379,6 @@ packageProductDependencies = ( AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */, AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */, - AAD720CF2D0816B200F6304D /* Alamofire */, - 961679522CFF207900B2B6DF /* SwiftProtobuf */, - 961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */, ); productName = "Coder Desktop"; productReference = 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */; @@ -333,6 +396,7 @@ ); dependencies = ( 961679112CFF100E00B2B6DF /* PBXTargetDependency */, + AA3B40BA2D2FDA5C0099996A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 961679122CFF100E00B2B6DF /* Coder DesktopTests */, @@ -340,6 +404,7 @@ name = "Coder DesktopTests"; packageProductDependencies = ( AA8BC3382D0060A900E1ABAA /* ViewInspector */, + AA3B40B52D2FD9DD0099996A /* Mocker */, ); productName = "Coder DesktopTests"; productReference = 9616790F2CFF100E00B2B6DF /* Coder DesktopTests.xctest */; @@ -404,6 +469,7 @@ buildRules = ( ); dependencies = ( + AA3B40C32D2FE7760099996A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( AA3B3DA22D2D23860099996A /* VPNLib */, @@ -442,6 +508,54 @@ productReference = AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + AA3B40902D2FC8560099996A /* CoderSDK */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3B40A72D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDK" */; + buildPhases = ( + AA3B408C2D2FC8560099996A /* Headers */, + AA3B408D2D2FC8560099996A /* Sources */, + AA3B408E2D2FC8560099996A /* Frameworks */, + AA3B408F2D2FC8560099996A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + AA3B40922D2FC8560099996A /* CoderSDK */, + ); + name = CoderSDK; + packageProductDependencies = ( + ); + productName = CoderSDK; + productReference = AA3B40912D2FC8560099996A /* CoderSDK.framework */; + productType = "com.apple.product-type.framework"; + }; + AA3B40972D2FC8560099996A /* CoderSDKTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3B40AA2D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDKTests" */; + buildPhases = ( + AA3B40942D2FC8560099996A /* Sources */, + AA3B40952D2FC8560099996A /* Frameworks */, + AA3B40962D2FC8560099996A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AA3B409B2D2FC8560099996A /* PBXTargetDependency */, + AA3B409D2D2FC8560099996A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + AA3B409E2D2FC8560099996A /* CoderSDKTests */, + ); + name = CoderSDKTests; + packageProductDependencies = ( + AA3B40BC2D2FDFBA0099996A /* Mocker */, + ); + productName = CoderSDKTests; + productReference = AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -473,6 +587,13 @@ CreatedOnToolsVersion = 16.2; TestTargetID = 961678FB2CFF100D00B2B6DF; }; + AA3B40902D2FC8560099996A = { + CreatedOnToolsVersion = 16.2; + }; + AA3B40972D2FC8560099996A = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 961678FB2CFF100D00B2B6DF; + }; }; }; buildConfigurationList = 961678F72CFF100D00B2B6DF /* Build configuration list for PBXProject "Coder Desktop" */; @@ -489,7 +610,6 @@ AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */, AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */, AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */, - AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */, 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */, AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */, ); @@ -504,6 +624,8 @@ 9616792F2CFF117300B2B6DF /* VPN */, AA3B3DA02D2D23860099996A /* VPNLib */, AA3B3DA72D2D23860099996A /* VPNLibTests */, + AA3B40902D2FC8560099996A /* CoderSDK */, + AA3B40972D2FC8560099996A /* CoderSDKTests */, ); }; /* End PBXProject section */ @@ -551,6 +673,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA3B408F2D2FC8560099996A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3B40962D2FC8560099996A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -596,6 +732,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA3B408D2D2FC8560099996A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3B40942D2FC8560099996A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -624,16 +774,36 @@ target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; targetProxy = AA3B3DAC2D2D23860099996A /* PBXContainerItemProxy */; }; - AA3B3DB32D2D23860099996A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = AA3B3DA02D2D23860099996A /* VPNLib */; - targetProxy = AA3B3DB22D2D23860099996A /* PBXContainerItemProxy */; - }; AA3B3DD02D2D249F0099996A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AA3B3DA02D2D23860099996A /* VPNLib */; targetProxy = AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */; }; + AA3B409B2D2FC8560099996A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA3B40902D2FC8560099996A /* CoderSDK */; + targetProxy = AA3B409A2D2FC8560099996A /* PBXContainerItemProxy */; + }; + AA3B409D2D2FC8560099996A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; + targetProxy = AA3B409C2D2FC8560099996A /* PBXContainerItemProxy */; + }; + AA3B40A32D2FC8560099996A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA3B40902D2FC8560099996A /* CoderSDK */; + targetProxy = AA3B40A22D2FC8560099996A /* PBXContainerItemProxy */; + }; + AA3B40BA2D2FDA5C0099996A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA3B40902D2FC8560099996A /* CoderSDK */; + targetProxy = AA3B40B92D2FDA5C0099996A /* PBXContainerItemProxy */; + }; + AA3B40C32D2FE7760099996A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA3B40902D2FC8560099996A /* CoderSDK */; + targetProxy = AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */; + }; AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = AA8BC33B2D0060E700E1ABAA /* SwiftLintBuildToolPlugin */; @@ -969,6 +1139,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; @@ -977,7 +1148,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1005,6 +1176,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; @@ -1013,7 +1185,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1071,6 +1243,114 @@ }; name = Release; }; + AA3B40A82D2FC8560099996A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 4399GN35BJ; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).CoderSDK"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + AA3B40A92D2FC8560099996A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 4399GN35BJ; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).CoderSDK"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + AA3B40AB2D2FC8560099996A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4399GN35BJ; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.CoderSDKTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"; + }; + name = Debug; + }; + AA3B40AC2D2FC8560099996A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4399GN35BJ; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.CoderSDKTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1137,6 +1417,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + AA3B40A72D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3B40A82D2FC8560099996A /* Debug */, + AA3B40A92D2FC8560099996A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA3B40AA2D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDKTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3B40AB2D2FC8560099996A /* Debug */, + AA3B40AC2D2FC8560099996A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1188,38 +1486,30 @@ kind = branch; }; }; - AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.10.2; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 961679522CFF207900B2B6DF /* SwiftProtobuf */ = { + AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */; productName = SwiftProtobuf; }; - 961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */ = { + AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */ = { isa = XCSwiftPackageProductDependency; package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */; productName = SwiftProtobufPluginLibrary; }; - AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */ = { + AA3B3E8D2D2E0FF40099996A /* Mocker */ = { isa = XCSwiftPackageProductDependency; - package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */; - productName = SwiftProtobuf; + package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */; + productName = Mocker; }; - AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */ = { + AA3B40B52D2FD9DD0099996A /* Mocker */ = { isa = XCSwiftPackageProductDependency; - package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */; - productName = SwiftProtobufPluginLibrary; + package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */; + productName = Mocker; }; - AA3B3E8D2D2E0FF40099996A /* Mocker */ = { + AA3B40BC2D2FDFBA0099996A /* Mocker */ = { isa = XCSwiftPackageProductDependency; package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */; productName = Mocker; @@ -1244,11 +1534,6 @@ package = AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; - AAD720CF2D0816B200F6304D /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 961678F42CFF100D00B2B6DF /* Project object */; diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ebb3ead..5a69cd2 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { - "originHash" : "1cd4f7368eeddbaa35ef829e13093bc7081a4e6d3da9492d22db0757464ad473", + "originHash" : "ec40e522ec1a2416e8e8f5cbe97424ab3e4a614e6ef453c10ea28e84e88b6771", "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire", - "state" : { - "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", - "version" : "5.10.2" - } - }, { "identity" : "fluid-menu-bar-extra", "kind" : "remoteSourceControl", diff --git a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme index e4f5432..c17080f 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme +++ b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme @@ -79,6 +79,17 @@ ReferencedContainer = "container:Coder Desktop.xcodeproj"> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO" + parallelizable = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "AA3B40972D2FC8560099996A" + BuildableName = "CoderSDKTests.xctest" + BlueprintName = "CoderSDKTests" + ReferencedContainer = "container:Coder Desktop.xcodeproj"> + </BuildableReference> + </TestableReference> </Testables> </TestAction> <LaunchAction diff --git a/Coder Desktop/Coder Desktop.xctestplan b/Coder Desktop/Coder Desktop.xctestplan index 0cef4af..a0f608b 100644 --- a/Coder Desktop/Coder Desktop.xctestplan +++ b/Coder Desktop/Coder Desktop.xctestplan @@ -19,8 +19,17 @@ { "target" : { "containerPath" : "container:Coder Desktop.xcodeproj", - "identifier" : "9616790E2CFF100E00B2B6DF", - "name" : "Coder DesktopTests" + "identifier" : "AA3B40972D2FC8560099996A", + "name" : "CoderSDKTests" + } + }, + { + "enabled" : false, + "parallelizable" : true, + "target" : { + "containerPath" : "container:Coder Desktop.xcodeproj", + "identifier" : "961679182CFF100E00B2B6DF", + "name" : "Coder DesktopUITests" } }, { @@ -31,12 +40,10 @@ } }, { - "enabled" : false, - "parallelizable" : true, "target" : { "containerPath" : "container:Coder Desktop.xcodeproj", - "identifier" : "961679182CFF100E00B2B6DF", - "name" : "Coder DesktopUITests" + "identifier" : "9616790E2CFF100E00B2B6DF", + "name" : "Coder DesktopTests" } } ], diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 408722b..7650386 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -11,7 +11,7 @@ struct DesktopApp: App { EmptyView() } Window("Sign In", id: Windows.login.rawValue) { - LoginForm<PreviewClient, PreviewSession>() + LoginForm<PreviewSession>() }.environmentObject(appDelegate.session) .windowResizability(.contentSize) } diff --git a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift deleted file mode 100644 index 7a9eef4..0000000 --- a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Alamofire -import SwiftUI - -struct PreviewClient: Client { - init(url _: URL, token _: String? = nil) {} - - func user(_: String) async throws(ClientError) -> User { - do { - try await Task.sleep(for: .seconds(1)) - return User( - id: UUID(), - username: "admin", - avatar_url: "", - name: "admin", - email: "admin@coder.com", - created_at: Date.now, - updated_at: Date.now, - last_seen_at: Date.now, - status: "active", - login_type: "none", - theme_preference: "dark", - organization_ids: [], - roles: [] - ) - } catch { - throw .reqError(.explicitlyCancelled) - } - } -} diff --git a/Coder Desktop/Coder Desktop/SDK/Client.swift b/Coder Desktop/Coder Desktop/SDK/Client.swift deleted file mode 100644 index 1facec2..0000000 --- a/Coder Desktop/Coder Desktop/SDK/Client.swift +++ /dev/null @@ -1,140 +0,0 @@ -import Alamofire -import Foundation - -protocol Client: Sendable { - init(url: URL, token: String?) - func user(_ ident: String) async throws(ClientError) -> User -} - -struct CoderClient: Client { - public let url: URL - public var token: String? - - static let decoder: JSONDecoder = { - var dec = JSONDecoder() - dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds - return dec - }() - - let encoder: JSONEncoder = { - var enc = JSONEncoder() - enc.dateEncodingStrategy = .iso8601withFractionalSeconds - return enc - }() - - func request<T: Encodable & Sendable>( - _ path: String, - method: HTTPMethod, - body: T? = nil - ) async throws(ClientError) -> HTTPResponse { - let url = self.url.appendingPathComponent(path) - let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] } - let out = await AF.request( - url, - method: method, - parameters: body, - headers: headers - ).serializingData().response - switch out.result { - case let .success(data): - return HTTPResponse(resp: out.response!, data: data, req: out.request) - case let .failure(error): - throw .reqError(error) - } - } - - func request( - _ path: String, - method: HTTPMethod - ) async throws(ClientError) -> HTTPResponse { - let url = self.url.appendingPathComponent(path) - let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] } - let out = await AF.request( - url, - method: method, - headers: headers - ).serializingData().response - switch out.result { - case let .success(data): - return HTTPResponse(resp: out.response!, data: data, req: out.request) - case let .failure(error): - throw .reqError(error) - } - } - - func responseAsError(_ resp: HTTPResponse) -> ClientError { - do { - let body = try CoderClient.decoder.decode(Response.self, from: resp.data) - let out = APIError( - response: body, - statusCode: resp.resp.statusCode, - method: resp.req?.httpMethod, - url: resp.req?.url - ) - return .apiError(out) - } catch { - return .unexpectedResponse(resp.data[...1024]) - } - } - - enum Headers { - static let sessionToken = "Coder-Session-Token" - } -} - -struct HTTPResponse { - let resp: HTTPURLResponse - let data: Data - let req: URLRequest? -} - -struct APIError: Decodable { - let response: Response - let statusCode: Int - let method: String? - let url: URL? - - var description: String { - var components: [String] = [] - if let method = method, let url = url { - components.append("\(method) \(url.absoluteString)") - } - components.append("Unexpected status code \(statusCode):\n\(response.message)") - if let detail = response.detail { - components.append("\tError: \(detail)") - } - if let validations = response.validations, !validations.isEmpty { - let validationMessages = validations.map { "\t\($0.field): \($0.detail)" } - components.append(contentsOf: validationMessages) - } - return components.joined(separator: "\n") - } -} - -struct Response: Decodable { - let message: String - let detail: String? - let validations: [FieldValidation]? -} - -struct FieldValidation: Decodable { - let field: String - let detail: String -} - -enum ClientError: Error { - case apiError(APIError) - case reqError(AFError) - case unexpectedResponse(Data) - - var description: String { - switch self { - case let .apiError(error): - return error.description - case let .reqError(error): - return error.localizedDescription - case let .unexpectedResponse(data): - return "Unexpected response: \(data)" - } - } -} diff --git a/Coder Desktop/Coder Desktop/SDK/User.swift b/Coder Desktop/Coder Desktop/SDK/User.swift deleted file mode 100644 index 9a4b906..0000000 --- a/Coder Desktop/Coder Desktop/SDK/User.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -extension CoderClient { - func user(_ ident: String) async throws(ClientError) -> User { - let res = try await request("/api/v2/users/\(ident)", method: .get) - guard res.resp.statusCode == 200 else { - throw responseAsError(res) - } - do { - return try CoderClient.decoder.decode(User.self, from: res.data) - } catch { - throw .unexpectedResponse(res.data[...1024]) - } - } -} - -struct User: Decodable { - let id: UUID - let username: String - let avatar_url: String - let name: String - let email: String - let created_at: Date - let updated_at: Date - let last_seen_at: Date - let status: String - let login_type: String - let theme_preference: String - let organization_ids: [UUID] - let roles: [Role] -} - -struct Role: Decodable { - let name: String - let display_name: String - let organization_id: UUID? -} diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder Desktop/Coder Desktop/Views/LoginForm.swift index ef9dbb5..a9b2e5f 100644 --- a/Coder Desktop/Coder Desktop/Views/LoginForm.swift +++ b/Coder Desktop/Coder Desktop/Views/LoginForm.swift @@ -1,6 +1,7 @@ +import CoderSDK import SwiftUI -struct LoginForm<C: Client, S: Session>: View { +struct LoginForm<S: Session>: View { @EnvironmentObject var session: S @Environment(\.dismiss) private var dismiss @@ -69,7 +70,7 @@ struct LoginForm<C: Client, S: Session>: View { } loading = true defer { loading = false } - let client = C(url: url, token: sessionToken) + let client = Client(url: url, token: sessionToken) do { _ = try await client.user("me") } catch { @@ -188,6 +189,6 @@ enum LoginField: Hashable { } #Preview { - LoginForm<PreviewClient, PreviewSession>() + LoginForm<PreviewSession>() .environmentObject(PreviewSession()) } diff --git a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift index ae77b5c..912f409 100644 --- a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift +++ b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift @@ -1,4 +1,6 @@ @testable import Coder_Desktop +@testable import CoderSDK +import Mocker import SwiftUI import Testing import ViewInspector @@ -7,12 +9,12 @@ import ViewInspector @Suite(.timeLimit(.minutes(1))) struct LoginTests { let session: MockSession - let sut: LoginForm<MockClient, MockSession> + let sut: LoginForm<MockSession> let view: any View init() { session = MockSession() - sut = LoginForm<MockClient, MockSession>() + sut = LoginForm<MockSession>() view = sut.environmentObject(session) } @@ -68,14 +70,16 @@ struct LoginTests { @Test func testFailedAuthentication() async throws { - let login = LoginForm<MockErrorClient, MockSession>() + let login = LoginForm<MockSession>() + let url = URL(string: "https://testFailedAuthentication.com")! + Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register() try await ViewHosting.host(login.environmentObject(session)) { try await login.inspection.inspect { view in - try view.find(ViewType.TextField.self).setInput("https://coder.example.com") + try view.find(ViewType.TextField.self).setInput(url.absoluteString) try view.find(button: "Next").tap() #expect(throws: Never.self) { try view.find(text: "Session Token") } - try view.find(ViewType.SecureField.self).setInput("valid-token") + try view.find(ViewType.SecureField.self).setInput("invalid-token") try await view.actualView().submit() #expect(throws: Never.self) { try view.find(ViewType.Alert.self) } } @@ -84,9 +88,33 @@ struct LoginTests { @Test func testSuccessfulLogin() async throws { + let url = URL(string: "https://testSuccessfulLogin.com")! + + let user = User( + id: UUID(), + username: "admin", + avatar_url: "", + name: "admin", + email: "admin@coder.com", + created_at: Date.now, + updated_at: Date.now, + last_seen_at: Date.now, + status: "active", + login_type: "none", + theme_preference: "dark", + organization_ids: [], + roles: [] + ) + + try Mock( + url: url.appendingPathComponent("/api/v2/users/me"), + statusCode: 200, + data: [.get: Client.encoder.encode(user)] + ).register() + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in - try view.find(ViewType.TextField.self).setInput("https://coder.example.com") + try view.find(ViewType.TextField.self).setInput(url.absoluteString) try view.find(button: "Next").tap() try view.find(ViewType.SecureField.self).setInput("valid-token") try await view.actualView().submit() diff --git a/Coder Desktop/Coder DesktopTests/Util.swift b/Coder Desktop/Coder DesktopTests/Util.swift index bb0ff99..244513e 100644 --- a/Coder Desktop/Coder DesktopTests/Util.swift +++ b/Coder Desktop/Coder DesktopTests/Util.swift @@ -27,7 +27,7 @@ class MockVPNService: VPNService, ObservableObject { class MockSession: Session { @Published - var hasSession: Bool = true + var hasSession: Bool = false @Published var sessionToken: String? = "fake-token" @Published @@ -50,33 +50,4 @@ class MockSession: Session { } } -struct MockClient: Client { - init(url _: URL, token _: String? = nil) {} - - func user(_: String) async throws(ClientError) -> Coder_Desktop.User { - User( - id: UUID(), - username: "admin", - avatar_url: "", - name: "admin", - email: "admin@coder.com", - created_at: Date.now, - updated_at: Date.now, - last_seen_at: Date.now, - status: "active", - login_type: "none", - theme_preference: "dark", - organization_ids: [], - roles: [] - ) - } -} - -struct MockErrorClient: Client { - init(url _: URL, token _: String?) {} - func user(_: String) async throws(ClientError) -> Coder_Desktop.User { - throw .reqError(.explicitlyCancelled) - } -} - extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {} diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift index 484cb3a..6aaf5b0 100644 --- a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift +++ b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift @@ -104,6 +104,7 @@ struct VPNMenuTests { @Test func testOffWhenFailed() async throws { + session.hasSession = true try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let toggle = try view.find(ViewType.Toggle.self) diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift new file mode 100644 index 0000000..59a17cb --- /dev/null +++ b/Coder Desktop/CoderSDK/Client.swift @@ -0,0 +1,137 @@ +import Foundation + +public struct Client { + public let url: URL + public var token: String? + public var headers: [HTTPHeader] + + public init(url: URL, token: String? = nil, headers: [HTTPHeader] = []) { + self.url = url + self.token = token + self.headers = headers + } + + static let decoder: JSONDecoder = { + var dec = JSONDecoder() + dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds + return dec + }() + + static let encoder: JSONEncoder = { + var enc = JSONEncoder() + enc.dateEncodingStrategy = .iso8601withFractionalSeconds + return enc + }() + + private func doRequest( + path: String, + method: HTTPMethod, + body: Data? = nil + ) async throws(ClientError) -> HTTPResponse { + let url = self.url.appendingPathComponent(path) + var req = URLRequest(url: url) + if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) } + req.httpMethod = method.rawValue + for header in headers { + req.addValue(header.value, forHTTPHeaderField: header.header) + } + req.httpBody = body + let data: Data + let resp: URLResponse + do { + (data, resp) = try await URLSession.shared.data(for: req) + } catch { + throw .network(error) + } + guard let httpResponse = resp as? HTTPURLResponse else { + throw .unexpectedResponse(data) + } + return HTTPResponse(resp: httpResponse, data: data, req: req) + } + + func request<T: Encodable & Sendable>( + _ path: String, + method: HTTPMethod, + body: T + ) async throws(ClientError) -> HTTPResponse { + let encodedBody: Data? + do { + encodedBody = try Client.encoder.encode(body) + } catch { + throw .encodeFailure(error) + } + return try await doRequest(path: path, method: method, body: encodedBody) + } + + func request( + _ path: String, + method: HTTPMethod + ) async throws(ClientError) -> HTTPResponse { + return try await doRequest(path: path, method: method) + } + + func responseAsError(_ resp: HTTPResponse) -> ClientError { + do { + let body = try Client.decoder.decode(Response.self, from: resp.data) + let out = APIError( + response: body, + statusCode: resp.resp.statusCode, + method: resp.req.httpMethod!, + url: resp.req.url! + ) + return .api(out) + } catch { + return .unexpectedResponse(resp.data.prefix(1024)) + } + } +} + +public struct APIError: Decodable { + let response: Response + let statusCode: Int + let method: String + let url: URL + + var description: String { + var components = ["\(method) \(url.absoluteString)\nUnexpected status code \(statusCode):\n\(response.message)"] + if let detail = response.detail { + components.append("\tError: \(detail)") + } + if let validations = response.validations, !validations.isEmpty { + let validationMessages = validations.map { "\t\($0.field): \($0.detail)" } + components.append(contentsOf: validationMessages) + } + return components.joined(separator: "\n") + } +} + +public struct Response: Decodable { + let message: String + let detail: String? + let validations: [FieldValidation]? +} + +public struct FieldValidation: Decodable { + let field: String + let detail: String +} + +public enum ClientError: Error { + case api(APIError) + case network(any Error) + case unexpectedResponse(Data) + case encodeFailure(any Error) + + public var description: String { + switch self { + case let .api(error): + return error.description + case let .network(error): + return error.localizedDescription + case let .unexpectedResponse(data): + return "Unexpected or non HTTP response: \(data)" + case let .encodeFailure(error): + return "Failed to encode body: \(error)" + } + } +} diff --git a/Coder Desktop/CoderSDK/CoderSDK.h b/Coder Desktop/CoderSDK/CoderSDK.h new file mode 100644 index 0000000..2d4371e --- /dev/null +++ b/Coder Desktop/CoderSDK/CoderSDK.h @@ -0,0 +1,11 @@ +#import <Foundation/Foundation.h> + +//! Project version number for CoderSDK. +FOUNDATION_EXPORT double CoderSDKVersionNumber; + +//! Project version string for CoderSDK. +FOUNDATION_EXPORT const unsigned char CoderSDKVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import <CoderSDK/PublicHeader.h> + + diff --git a/Coder Desktop/Coder Desktop/SDK/Date.swift b/Coder Desktop/CoderSDK/Date.swift similarity index 86% rename from Coder Desktop/Coder Desktop/SDK/Date.swift rename to Coder Desktop/CoderSDK/Date.swift index 05d536f..c8d7af7 100644 --- a/Coder Desktop/Coder Desktop/SDK/Date.swift +++ b/Coder Desktop/CoderSDK/Date.swift @@ -28,3 +28,9 @@ extension JSONEncoder.DateEncodingStrategy { try container.encode($0.formatted(.iso8601withFractionalSeconds)) } } + +public extension Date { + static func == (lhs: Date, rhs: Date) -> Bool { + abs(lhs.timeIntervalSince1970 - rhs.timeIntervalSince1970) < 0.001 + } +} diff --git a/Coder Desktop/CoderSDK/Deployment.swift b/Coder Desktop/CoderSDK/Deployment.swift new file mode 100644 index 0000000..ea1d23c --- /dev/null +++ b/Coder Desktop/CoderSDK/Deployment.swift @@ -0,0 +1,32 @@ +public extension Client { + func buildInfo() async throws(ClientError) -> BuildInfoResponse { + let res = try await request("/api/v2/buildinfo", method: .get) + guard res.resp.statusCode == 200 else { + throw responseAsError(res) + } + do { + return try Client.decoder.decode(BuildInfoResponse.self, from: res.data) + } catch { + throw .unexpectedResponse(res.data.prefix(1024)) + } + } +} + +public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable { + public let external_url: String + public let version: String + public let dashboard_url: String + public let telemetry: Bool + public let workspace_proxy: Bool + public let agent_api_version: String + public let provisioner_api_version: String + public let upgrade_message: String + public let deployment_id: String + + // `version` in the form `[0-9]+.[0-9]+.[0-9]+` + public var semver: String? { + return try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#) + .firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version)) + .flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } } + } +} diff --git a/Coder Desktop/CoderSDK/HTTP.swift b/Coder Desktop/CoderSDK/HTTP.swift new file mode 100644 index 0000000..94b8cde --- /dev/null +++ b/Coder Desktop/CoderSDK/HTTP.swift @@ -0,0 +1,26 @@ +public struct HTTPResponse { + let resp: HTTPURLResponse + let data: Data + let req: URLRequest +} + +public struct HTTPHeader: Sendable { + public let header: String + public let value: String + public init(header: String, value: String) { + self.header = header + self.value = value + } +} + +enum HTTPMethod: String, Equatable, Hashable, Sendable { + case get = "GET" + case post = "POST" + case delete = "DELETE" + case put = "PUT" + case head = "HEAD" +} + +enum Headers { + static let sessionToken = "Coder-Session-Token" +} diff --git a/Coder Desktop/CoderSDK/User.swift b/Coder Desktop/CoderSDK/User.swift new file mode 100644 index 0000000..e7f85f4 --- /dev/null +++ b/Coder Desktop/CoderSDK/User.swift @@ -0,0 +1,73 @@ +import Foundation + +public extension Client { + func user(_ ident: String) async throws(ClientError) -> User { + let res = try await request("/api/v2/users/\(ident)", method: .get) + guard res.resp.statusCode == 200 else { + throw responseAsError(res) + } + do { + return try Client.decoder.decode(User.self, from: res.data) + } catch { + throw .unexpectedResponse(res.data.prefix(1024)) + } + } +} + +public struct User: Encodable, Decodable, Equatable, Sendable { + public let id: UUID + public let username: String + public let avatar_url: String + public let name: String + public let email: String + public let created_at: Date + public let updated_at: Date + public let last_seen_at: Date + public let status: String + public let login_type: String + public let theme_preference: String + public let organization_ids: [UUID] + public let roles: [Role] + + public init( + id: UUID, + username: String, + avatar_url: String, + name: String, + email: String, + created_at: Date, + updated_at: Date, + last_seen_at: Date, + status: String, + login_type: String, + theme_preference: String, + organization_ids: [UUID], + roles: [Role] + ) { + self.id = id + self.username = username + self.avatar_url = avatar_url + self.name = name + self.email = email + self.created_at = created_at + self.updated_at = updated_at + self.last_seen_at = last_seen_at + self.status = status + self.login_type = login_type + self.theme_preference = theme_preference + self.organization_ids = organization_ids + self.roles = roles + } +} + +public struct Role: Encodable, Decodable, Equatable, Sendable { + public let name: String + public let display_name: String + public let organization_id: UUID? + + public init(name: String, display_name: String, organization_id: UUID?) { + self.name = name + self.display_name = display_name + self.organization_id = organization_id + } +} diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift new file mode 100644 index 0000000..33c61c4 --- /dev/null +++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift @@ -0,0 +1,76 @@ +@testable import CoderSDK +import Mocker +import Testing + +@Suite(.timeLimit(.minutes(1))) +struct CoderSDKTests { + @Test + func user() async throws { + let now = Date.now + let user = User( + id: UUID(), + username: "johndoe", + avatar_url: "https://example.com/img.png", + name: "John Doe", + email: "john.doe@example.com", + created_at: now, + updated_at: now, + last_seen_at: now, + status: "active", + login_type: "email", + theme_preference: "dark", + organization_ids: [UUID()], + roles: [ + Role(name: "user", display_name: "User", organization_id: UUID()), + ] + ) + + let url = URL(string: "https://example.com")! + let token = "fake-token" + let client = Client(url: url, token: token, headers: [.init(header: "X-Test-Header", value: "foo")]) + var mock = try Mock( + url: url.appending(path: "api/v2/users/johndoe"), + contentType: .json, + statusCode: 200, + data: [.get: Client.encoder.encode(user)] + ) + var correctHeaders = false + mock.onRequestHandler = OnRequestHandler { req in + correctHeaders = req.value(forHTTPHeaderField: Headers.sessionToken) == token && + req.value(forHTTPHeaderField: "X-Test-Header") == "foo" + } + mock.register() + + let retUser = try await client.user(user.username) + #expect(user == retUser) + #expect(correctHeaders) + } + + @Test + func buildInfo() async throws { + let buildInfo = BuildInfoResponse( + external_url: "https://example.com", + version: "v2.18.2-devel+630fd7c0a", + dashboard_url: "https://example.com/dashboard", + telemetry: true, + workspace_proxy: false, + agent_api_version: "1.0", + provisioner_api_version: "1.2", + upgrade_message: "foo", + deployment_id: UUID().uuidString + ) + + let url = URL(string: "https://example.com")! + let client = Client(url: url) + try Mock( + url: url.appending(path: "api/v2/buildinfo"), + contentType: .json, + statusCode: 200, + data: [.get: Client.encoder.encode(buildInfo)] + ).register() + + let retBuildInfo = try await client.buildInfo() + #expect(buildInfo == retBuildInfo) + #expect(retBuildInfo.semver == "2.18.2") + } +} diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index d980d1c..bd598a0 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -1,3 +1,4 @@ +import CoderSDK import NetworkExtension import os import VPNLib @@ -30,8 +31,18 @@ actor Manager { } catch { throw .download(error) } + let client = Client(url: cfg.serverUrl) + let buildInfo: BuildInfoResponse do { - try SignatureValidator.validate(path: dest) + buildInfo = try await client.buildInfo() + } catch { + throw .serverInfo(error.description) + } + guard let semver = buildInfo.semver else { + throw .serverInfo("invalid version: \(buildInfo.version)") + } + do { + try SignatureValidator.validate(path: dest, expectedVersion: semver) } catch { throw .validation(error) } @@ -181,6 +192,7 @@ enum ManagerError: Error { case validation(ValidationError) case incorrectResponse(Vpn_TunnelMessage) case failedRPC(any Error) + case serverInfo(String) case errorResponse(msg: String) case noTunnelFileDescriptor } diff --git a/Coder Desktop/VPNLib/Download.swift b/Coder Desktop/VPNLib/Download.swift index 729ba05..75ff91b 100644 --- a/Coder Desktop/VPNLib/Download.swift +++ b/Coder Desktop/VPNLib/Download.swift @@ -37,7 +37,6 @@ public class SignatureValidator { private static let expectedName = "CoderVPN" private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib" private static let expectedTeamIdentifier = "4399GN35BJ" - private static let minDylibVersion = "2.18.1" private static let infoIdentifierKey = "CFBundleIdentifier" private static let infoNameKey = "CFBundleName" @@ -45,7 +44,8 @@ public class SignatureValidator { private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) - public static func validate(path: URL) throws(ValidationError) { + // `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+` + public static func validate(path: URL, expectedVersion: String) throws(ValidationError) { guard FileManager.default.fileExists(atPath: path.path) else { throw .fileNotFound } @@ -94,7 +94,7 @@ public class SignatureValidator { } guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, - minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending + expectedVersion.compare(dylibVersion, options: .numeric) != .orderedDescending else { throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String) }