From a3d8ac9fec2fa540a64d9fb2d6c344ef1e30fe4a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 10 Jan 2025 14:48:50 +1100 Subject: [PATCH 1/5] chore: extract CoderSDK to framework --- .../Coder Desktop.xcodeproj/project.pbxproj | 401 +++++++++++++++--- .../xcshareddata/swiftpm/Package.resolved | 11 +- .../xcschemes/Coder Desktop.xcscheme | 11 + Coder Desktop/Coder Desktop.xctestplan | 19 +- .../Coder Desktop/Coder_DesktopApp.swift | 2 +- .../Preview Content/PreviewClient.swift | 29 -- Coder Desktop/Coder Desktop/SDK/Client.swift | 140 ------ Coder Desktop/Coder Desktop/SDK/User.swift | 37 -- .../Coder Desktop/Views/LoginForm.swift | 7 +- .../Coder DesktopTests/LoginFormTests.swift | 40 +- Coder Desktop/Coder DesktopTests/Util.swift | 31 +- .../Coder DesktopTests/VPNMenuTests.swift | 1 + Coder Desktop/CoderSDK/Client.swift | 136 ++++++ Coder Desktop/CoderSDK/CoderSDK.h | 11 + .../SDK => CoderSDK}/Date.swift | 6 + Coder Desktop/CoderSDK/Deployment.swift | 32 ++ Coder Desktop/CoderSDK/HTTP.swift | 17 + Coder Desktop/CoderSDK/User.swift | 73 ++++ .../CoderSDKTests/CoderSDKTests.swift | 75 ++++ Coder Desktop/VPN/Manager.swift | 14 +- Coder Desktop/VPNLib/Download.swift | 6 +- 21 files changed, 774 insertions(+), 325 deletions(-) delete mode 100644 Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift delete mode 100644 Coder Desktop/Coder Desktop/SDK/Client.swift delete mode 100644 Coder Desktop/Coder Desktop/SDK/User.swift create mode 100644 Coder Desktop/CoderSDK/Client.swift create mode 100644 Coder Desktop/CoderSDK/CoderSDK.h rename Coder Desktop/{Coder Desktop/SDK => CoderSDK}/Date.swift (86%) create mode 100644 Coder Desktop/CoderSDK/Deployment.swift create mode 100644 Coder Desktop/CoderSDK/HTTP.swift create mode 100644 Coder Desktop/CoderSDK/User.swift create mode 100644 Coder Desktop/CoderSDKTests/CoderSDKTests.swift diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 9ecb95e..32930fb 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 = ""; }; + AA3B40922D2FC8560099996A /* CoderSDK */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + AA3B40A62D2FC8560099996A /* Exceptions for "CoderSDK" folder in "CoderSDK" target */, + ); + path = CoderSDK; + sourceTree = ""; + }; + AA3B409E2D2FC8560099996A /* CoderSDKTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CoderSDKTests; + sourceTree = ""; + }; 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 = ""; @@ -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,112 @@ }; 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; + 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; + 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 +1415,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 +1484,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 +1532,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"> + + + + () + LoginForm() }.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( - _ 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: View { +struct LoginForm: View { @EnvironmentObject var session: S @Environment(\.dismiss) private var dismiss @@ -69,7 +70,7 @@ struct LoginForm: 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() + LoginForm() .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 + let sut: LoginForm let view: any View init() { session = MockSession() - sut = LoginForm() + sut = LoginForm() view = sut.environmentObject(session) } @@ -68,14 +70,16 @@ struct LoginTests { @Test func testFailedAuthentication() async throws { - let login = LoginForm() + let login = LoginForm() + 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..bf9de45 --- /dev/null +++ b/Coder Desktop/CoderSDK/Client.swift @@ -0,0 +1,136 @@ +import Foundation + +public struct Client { + public let url: URL + public var token: String? + + public init(url: URL, token: String? = nil) { + self.url = url + self.token = token + } + + static let decoder: JSONDecoder = { + var dec = JSONDecoder() + dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds + return dec + }() + + static let encoder: JSONEncoder = { + var enc = JSONEncoder() + enc.dateEncodingStrategy = .iso8601withFractionalSeconds + return enc + }() + + func request( + _ path: String, + method: HTTPMethod, + body: T? = 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 + do { + if let body { req.httpBody = try Client.encoder.encode(body) } + } catch { + throw .encodeFailure + } + 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( + _ path: String, + method: HTTPMethod + ) 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 + 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 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 + + 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 .encodeFailure: + return "Failed to encode body" + } + } +} 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 + +//! 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 + + 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..0572b74 --- /dev/null +++ b/Coder Desktop/CoderSDK/HTTP.swift @@ -0,0 +1,17 @@ +public struct HTTPResponse { + let resp: HTTPURLResponse + let data: Data + let req: URLRequest +} + +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..e7fecb8 --- /dev/null +++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift @@ -0,0 +1,75 @@ +@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) + var mock = try Mock( + url: url.appending(path: "api/v2/users/johndoe"), + contentType: .json, + statusCode: 200, + data: [.get: Client.encoder.encode(user)] + ) + var tokenSent = false + mock.onRequestHandler = OnRequestHandler { req in + tokenSent = req.value(forHTTPHeaderField: Headers.sessionToken) == token + } + mock.register() + + let retUser = try await client.user(user.username) + #expect(user == retUser) + #expect(tokenSent) + } + + @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) } From 84936d0a011f4d4b6c6d46996c2abada4b875030 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 14 Jan 2025 15:53:05 +1100 Subject: [PATCH 2/5] tests target macos 14.6 --- Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 32930fb..6a26723 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -1324,6 +1324,7 @@ 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)"; @@ -1340,6 +1341,7 @@ 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)"; From 8b3ee9b9313da8741674f1ce83f09ebc99da4b78 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 16 Jan 2025 18:13:06 +1100 Subject: [PATCH 3/5] add custom header support --- Coder Desktop/CoderSDK/Client.swift | 51 +++++++++---------- Coder Desktop/CoderSDK/HTTP.swift | 5 ++ .../CoderSDKTests/CoderSDKTests.swift | 9 ++-- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift index bf9de45..bb15b4c 100644 --- a/Coder Desktop/CoderSDK/Client.swift +++ b/Coder Desktop/CoderSDK/Client.swift @@ -3,10 +3,12 @@ import Foundation public struct Client { public let url: URL public var token: String? + public var headers: [HTTPHeader] - public init(url: URL, token: String? = nil) { + public init(url: URL, token: String? = nil, headers: [HTTPHeader] = []) { self.url = url self.token = token + self.headers = headers } static let decoder: JSONDecoder = { @@ -21,20 +23,17 @@ public struct Client { return enc }() - func request( - _ path: String, + private func doRequest( + path: String, method: HTTPMethod, - body: T? = nil + 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 - do { - if let body { req.httpBody = try Client.encoder.encode(body) } - } catch { - throw .encodeFailure - } + for header in headers { req.addValue(header.value, forHTTPHeaderField: header.header) } + req.httpBody = body let data: Data let resp: URLResponse do { @@ -48,25 +47,25 @@ public struct Client { return HTTPResponse(resp: httpResponse, data: data, req: req) } - func request( + func request( _ path: String, - method: HTTPMethod + method: HTTPMethod, + body: T ) 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 - let data: Data - let resp: URLResponse + let encodedBody: Data? do { - (data, resp) = try await URLSession.shared.data(for: req) + encodedBody = try Client.encoder.encode(body) } catch { - throw .network(error) + throw .encodeFailure(error) } - guard let httpResponse = resp as? HTTPURLResponse else { - throw .unexpectedResponse(data) - } - return HTTPResponse(resp: httpResponse, data: data, req: req) + 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 { @@ -119,7 +118,7 @@ public enum ClientError: Error { case api(APIError) case network(any Error) case unexpectedResponse(Data) - case encodeFailure + case encodeFailure(any Error) public var description: String { switch self { @@ -129,8 +128,8 @@ public enum ClientError: Error { return error.localizedDescription case let .unexpectedResponse(data): return "Unexpected or non HTTP response: \(data)" - case .encodeFailure: - return "Failed to encode body" + case let .encodeFailure(error): + return "Failed to encode body: \(error)" } } } diff --git a/Coder Desktop/CoderSDK/HTTP.swift b/Coder Desktop/CoderSDK/HTTP.swift index 0572b74..c5b6832 100644 --- a/Coder Desktop/CoderSDK/HTTP.swift +++ b/Coder Desktop/CoderSDK/HTTP.swift @@ -4,6 +4,11 @@ public struct HTTPResponse { let req: URLRequest } +public struct HTTPHeader { + let header: String + let value: String +} + enum HTTPMethod: String, Equatable, Hashable, Sendable { case get = "GET" case post = "POST" diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift index e7fecb8..e10e7c5 100644 --- a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift +++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift @@ -27,22 +27,23 @@ struct CoderSDKTests { let url = URL(string: "https://example.com")! let token = "fake-token" - let client = Client(url: url, token: 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 tokenSent = false + var correctHeaders = false mock.onRequestHandler = OnRequestHandler { req in - tokenSent = req.value(forHTTPHeaderField: Headers.sessionToken) == token + 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(tokenSent) + #expect(correctHeaders) } @Test From a815ca7938664eaba61e661fa2cd10d9beda0472 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 16 Jan 2025 18:34:30 +1100 Subject: [PATCH 4/5] public sendable httpheader --- Coder Desktop/CoderSDK/HTTP.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Coder Desktop/CoderSDK/HTTP.swift b/Coder Desktop/CoderSDK/HTTP.swift index c5b6832..94b8cde 100644 --- a/Coder Desktop/CoderSDK/HTTP.swift +++ b/Coder Desktop/CoderSDK/HTTP.swift @@ -4,9 +4,13 @@ public struct HTTPResponse { let req: URLRequest } -public struct HTTPHeader { - let header: String - let value: String +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 { From 14fe075857476d7193cb361bbb8d97bd3763ac32 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 16 Jan 2025 18:35:51 +1100 Subject: [PATCH 5/5] fmt --- Coder Desktop/CoderSDK/Client.swift | 4 +++- Coder Desktop/CoderSDKTests/CoderSDKTests.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift index bb15b4c..59a17cb 100644 --- a/Coder Desktop/CoderSDK/Client.swift +++ b/Coder Desktop/CoderSDK/Client.swift @@ -32,7 +32,9 @@ public struct Client { 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) } + for header in headers { + req.addValue(header.value, forHTTPHeaderField: header.header) + } req.httpBody = body let data: Data let resp: URLResponse diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift index e10e7c5..33c61c4 100644 --- a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift +++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift @@ -37,7 +37,7 @@ struct CoderSDKTests { var correctHeaders = false mock.onRequestHandler = OnRequestHandler { req in correctHeaders = req.value(forHTTPHeaderField: Headers.sessionToken) == token && - req.value(forHTTPHeaderField: "X-Test-Header") == "foo" + req.value(forHTTPHeaderField: "X-Test-Header") == "foo" } mock.register()