Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signout command #140

Merged
merged 2 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 "No authenticated session found"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
Do you think the red is a little harsh for the error? It really just is an info statement. Perhaps the description here would be better a little less error like. You are already signed out ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like both suggestions 👍
Implemented it in d49c722

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
24 changes: 23 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,28 @@ 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 Signout.exit(withLegibleError: error) }

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)
}

}