Skip to content

Commit

Permalink
Merge pull request #140 from senmu/add-signout-command
Browse files Browse the repository at this point in the history
Add signout command
  • Loading branch information
Sam Lu authored Mar 18, 2021
2 parents c9c1784 + d49c722 commit d185259
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Xcode will be installed to /Applications by default, but you can provide the pat
- `uninstall`: Uninstall a specific version of Xcode
- `update`: Update the list of available versions of Xcode
- `version`: Print the version number of xcodes itself
- `signout`: Clears the stored username and password

### Shell Completion Scripts

Expand Down
3 changes: 3 additions & 0 deletions Sources/AppleAPI/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class Client {
case unexpectedSignInResponse(statusCode: Int, message: String?)
case appleIDAndPrivacyAcknowledgementRequired
case noTrustedPhoneNumbers
case notAuthenticated

public var errorDescription: String? {
switch self {
Expand All @@ -27,6 +28,8 @@ public class Client {
return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")."
case .noTrustedPhoneNumbers:
return "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915."
case .notAuthenticated:
return "You are already signed out"
default:
return String(describing: self)
}
Expand Down
19 changes: 19 additions & 0 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,25 @@ public final class XcodeInstaller {
}
}
}

public func logout() -> Promise<Void> {
guard let username = findUsername() else { return Promise<Void>(error: Client.Error.notAuthenticated) }

return Promise { seal in
// Remove cookies in the shared URLSession
AppleAPI.Current.network.session.reset {
seal.fulfill(())
}
}
.done {
// Remove all keychain items
try Current.keychain.remove(username)

// Set `defaultUsername` in Configuration to nil
self.configuration.defaultUsername = nil
try self.configuration.save()
}
}

let xcodesUsername = "XCODES_USERNAME"
let xcodesPassword = "XCODES_PASSWORD"
Expand Down
27 changes: 26 additions & 1 deletion Sources/xcodes/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct Xcodes: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Manage the Xcodes installed on your Mac",
shouldDisplay: true,
subcommands: [Download.self, Install.self, Installed.self, List.self, Select.self, Uninstall.self, Update.self, Version.self]
subcommands: [Download.self, Install.self, Installed.self, List.self, Select.self, Uninstall.self, Update.self, Version.self, Signout.self]
)

static var xcodesConfiguration = Configuration()
Expand Down Expand Up @@ -422,6 +422,31 @@ struct Xcodes: ParsableCommand {
Current.logging.log(XcodesKit.version.description)
}
}

struct Signout: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Clears the stored username and password"
)

@OptionGroup
var globalColor: GlobalColorOption

func run() {
Rainbow.enabled = Rainbow.enabled && globalColor.color

installer.logout()
.done {
Current.logging.log("Successfully signed out".green)
Signout.exit()
}
.recover { error in
Current.logging.log(error.legibleLocalizedDescription)
Signout.exit()
}

RunLoop.current.run()
}
}
}

// @main doesn't work yet because of https://bugs.swift.org/browse/SR-12683
Expand Down
42 changes: 42 additions & 0 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1212,5 +1212,47 @@ final class XcodesKitTests: XCTestCase {
"""
)
}

func test_Signout_WithExistingSession() {
var keychainDidRemove = false
Current.keychain.remove = { _ in
keychainDidRemove = true
}

var customConfig = Configuration()
customConfig.defaultUsername = "test@example.com"
let customInstaller = XcodeInstaller(configuration: customConfig, xcodeList: XcodeList())

let expectation = self.expectation(description: "Signout complete")

customInstaller.logout()
.ensure { expectation.fulfill() }
.catch {
XCTFail($0.localizedDescription)
}

waitForExpectations(timeout: 1.0)

XCTAssertTrue(keychainDidRemove)
}

func test_Signout_WithoutExistingSession() {
var customConfig = Configuration()
customConfig.defaultUsername = nil
let customInstaller = XcodeInstaller(configuration: customConfig, xcodeList: XcodeList())

var capturedError: Error?

let expectation = self.expectation(description: "Signout complete")

customInstaller.logout()
.ensure { expectation.fulfill() }
.catch { error in
capturedError = error
}
waitForExpectations(timeout: 1.0)

XCTAssertEqual(capturedError as? Client.Error, Client.Error.notAuthenticated)
}

}

0 comments on commit d185259

Please sign in to comment.