├── .circle.yml ├── .codecov.yml ├── .gitignore ├── .sourcery.yml ├── .sourcery └── LinuxMain.stencil ├── .travis.yml ├── Autobahn.swift ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Shuttle.md ├── Sources ├── DevPortal │ ├── DevPortal+API.swift │ ├── DevPortal+Client.swift │ ├── DevPortal.swift │ ├── DevPortalBase.swift │ └── Models │ │ ├── App.swift │ │ ├── AppGroup.swift │ │ ├── AppService.swift │ │ ├── Certificate.swift │ │ ├── Client.swift │ │ ├── Device.swift │ │ ├── Invite.swift │ │ ├── Key.swift │ │ ├── Merchant.swift │ │ ├── Passbook.swift │ │ ├── Person.swift │ │ └── ProvisioningProfile.swift ├── Shuttle │ ├── ProvisioningProfile.swift │ └── Shuttle.swift ├── ShuttleCore │ ├── AlamofireDecodableError.swift │ ├── AppleAuth+API.swift │ ├── Base.swift │ ├── Client+TwoStepAuth.swift │ ├── Client.swift │ ├── Core.swift │ ├── Extensions │ │ ├── DataRequest+Decodable.swift │ │ ├── MoyaProvider+Extension.swift │ │ ├── MoyaSugar+Extension.swift │ │ ├── RequestCSRFPlugin.swift │ │ └── Response+Decodable.swift │ ├── Models │ │ ├── AuthService.swift │ │ ├── HelpLink.swift │ │ ├── ITCModule.swift │ │ ├── OlympusSessionResponse.swift │ │ ├── Team.swift │ │ ├── User.swift │ │ └── UserDetailsResponse.swift │ ├── Olympus+API.swift │ └── TunesCore+API.swift ├── ShuttleDevelopment │ └── main.swift ├── TestFlight │ ├── Models │ │ ├── AppTestInfo.swift │ │ ├── BetaReviewInfo.swift │ │ ├── Build.swift │ │ ├── BuildTrains.swift │ │ ├── ExportCompliance.swift │ │ ├── Group.swift │ │ ├── TestInfo.swift │ │ └── Tester.swift │ ├── TestFlight+API.swift │ ├── TestFlight+Client.swift │ └── TestFlight.swift ├── TestSupport │ └── NetworkResponseStubs.swift └── Tunes │ ├── Models │ ├── App.swift │ ├── IAP.swift │ └── Member.swift │ ├── Tunes+Client.swift │ └── Tunes.swift ├── Tests ├── DevPortalTests │ ├── DevPortalTests.swift │ └── ProvisioningProfileTests.swift ├── LinuxMain.swift ├── ShuttleCoreTests │ ├── ClientTests.swift │ ├── DecodableTests.swift │ └── TwoStepAuthTests.swift ├── ShuttleTests │ └── ShuttleTests.swift ├── TestFlightTests │ ├── BuildTests.swift │ └── TestFlightTests.swift └── TunesTests │ └── TunesTests.swift └── Vagrantfile /.circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - sudo apt-get install swift 4 | - sudo chmod -R a+rx /usr/ 5 | test: 6 | override: 7 | - swift build 8 | - swift build -c release 9 | - swift test 10 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 0...100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | *.generated.swift 6 | 7 | *.log 8 | .vagrant -------------------------------------------------------------------------------- /.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - Tests 3 | templates: 4 | - .sourcery 5 | args: 6 | testImports: 7 | - "@testable import ShuttleTests" 8 | -------------------------------------------------------------------------------- /.sourcery/LinuxMain.stencil: -------------------------------------------------------------------------------- 1 | // sourcery:file:Tests/LinuxMain.swift 2 | import XCTest 3 | 4 | {% for testImport in argument.testImports %} 5 | {{ testImport }} 6 | {% endfor %} 7 | 8 | {% for type in types.classes|based:"XCTestCase" %} 9 | {% if not type.annotations.disableTests %}extension {{ type.name }} { 10 | static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [ 11 | {% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %} 12 | {% endif %}{% endfor %}] 13 | } 14 | 15 | {% endif %}{% endfor %} 16 | XCTMain([ 17 | {% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.disableTests %} testCase({{ type.name }}.allTests), 18 | {% endif %}{% endfor %}]) 19 | // sourcery:end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | 8 | osx_image: xcode9.0 9 | 10 | script: 11 | - swift build 12 | - swift build -c release 13 | - swift test 14 | -------------------------------------------------------------------------------- /Autobahn.swift: -------------------------------------------------------------------------------- 1 | import AutobahnDescription 2 | 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | * Our Responsibilities 25 | 26 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 27 | 28 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 29 | 30 | ## Scope 31 | 32 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 33 | 34 | ## Enforcement 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 37 | 38 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 39 | 40 | ## Attribution 41 | 42 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Kaden Wilkinson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "bc973c5311ce3db3f01a9fcde027fb11fa2254bf", 10 | "version": "4.6.0" 11 | } 12 | }, 13 | { 14 | "package": "Immutable", 15 | "repositoryURL": "https://github.com/devxoul/Immutable.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "b2f80e1f5f54d69c19458659df5aa30b7bcc885c", 19 | "version": "0.5.0" 20 | } 21 | }, 22 | { 23 | "package": "Moya", 24 | "repositoryURL": "https://github.com/Moya/Moya.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "7c599570c2a60079dfefcc27fc67cb303feb48e1", 28 | "version": "10.0.2" 29 | } 30 | }, 31 | { 32 | "package": "MoyaSugar", 33 | "repositoryURL": "https://github.com/devxoul/MoyaSugar.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "756dfb78234ba630902bf6d6884c9bff7d8b5eab", 37 | "version": "1.1.0" 38 | } 39 | }, 40 | { 41 | "package": "Rainbow", 42 | "repositoryURL": "https://github.com/onevcat/Rainbow", 43 | "state": { 44 | "branch": null, 45 | "revision": "f69961599ad524251d677fbec9e4bac57385d6fc", 46 | "version": "3.1.1" 47 | } 48 | }, 49 | { 50 | "package": "ReactiveSwift", 51 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "b9d5b350a446b85704396ce332a1f9e4960cfc6b", 55 | "version": "2.0.1" 56 | } 57 | }, 58 | { 59 | "package": "Result", 60 | "repositoryURL": "https://github.com/antitypical/Result.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "7477584259bfce2560a19e06ad9f71db441fff11", 64 | "version": "3.2.4" 65 | } 66 | }, 67 | { 68 | "package": "RxSwift", 69 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "e479d0029db0575e03e3c092e1c9724b9410eab7", 73 | "version": "4.1.1" 74 | } 75 | }, 76 | { 77 | "package": "Then", 78 | "repositoryURL": "https://github.com/devxoul/Then.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "c9941c72b8556000b7eb1baacf5f22b2a7925f92", 82 | "version": "2.3.0" 83 | } 84 | } 85 | ] 86 | }, 87 | "version": 1 88 | } 89 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Shuttle", 8 | products: [ 9 | .library(name: "Shuttle", targets: ["Shuttle"]), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/devxoul/MoyaSugar.git", .upToNextMajor(from: "1.1.0")), 13 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "10.0.2")), 14 | .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0"), 15 | ], 16 | targets: [ 17 | .target(name: "ShuttleDevelopment", dependencies: ["Shuttle"]), 18 | .target(name: "Shuttle", dependencies: ["TestFlight", "Tunes", "DevPortal"]), 19 | .target(name: "TestFlight", dependencies: ["ShuttleCore"]), 20 | .target(name: "Tunes", dependencies: ["ShuttleCore"]), 21 | .target(name: "DevPortal", dependencies: ["ShuttleCore"]), 22 | .target(name: "ShuttleCore", dependencies: ["MoyaSugar", "Moya", "Rainbow"]), 23 | .target(name: "TestSupport", dependencies: []), 24 | 25 | .testTarget(name: "ShuttleTests", dependencies: ["Shuttle", "TestSupport"]), 26 | .testTarget(name: "TestFlightTests", dependencies: ["TestFlight", "TestSupport"]), 27 | .testTarget(name: "TunesTests", dependencies: ["Tunes", "TestSupport"]), 28 | .testTarget(name: "DevPortalTests", dependencies: ["DevPortal", "TestSupport"]), 29 | .testTarget(name: "ShuttleCoreTests", dependencies: ["ShuttleCore", "TestSupport"]), 30 | ], 31 | swiftLanguageVersions: [3, 4] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Shuttle

2 | 3 |

4 | 5 | Travis status 6 | 7 | 8 | CircleCI status 9 | 10 | 11 | Code coverage 12 | 13 | 14 | Version 15 | 16 | 17 | Swift 4.0 18 | 19 | 20 | Docs 21 | 22 | 23 | License 24 | 25 | 26 | Twitter: @kdawgwilk 27 | 28 |

29 | 30 | This basically a port of fastlane's [spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) which is an HTTP client for interacting with the Apple Developer portal and iTunesConnect. 31 | 32 | >NOTE: This is still a work in progress and there is still much to do, here is a rough list of things I would like to see in the near future 33 | 34 | ## Example 35 | 36 | To see an example of the currently available APIs available see [Sources/Development/main.swift](Sources/Development/main.swift) 37 | 38 | ### ToDO List: 39 | 40 | - [ ] Support all API endpoints [listed below](#api-endpoints) 41 | - [ ] >90% Code Coverage 42 | - [ ] CLI tool 43 | 44 | 45 | ## API Endpoints 46 | 47 | Overview of the used API endpoints 48 | 49 | - `https://idmsa.apple.com`: Used to authenticate to get a valid session 50 | - `https://developerservices2.apple.com`: 51 | - Get a list of all available provisioning profiles 52 | - Register new devices 53 | - `https://developer.apple.com`: 54 | - List all devices, certificates, apps and app groups 55 | - Create new certificates, provisioning profiles and apps 56 | - Disable/enable services on apps and assign them to app groups 57 | - Delete certificates and apps 58 | - Repair provisioning profiles 59 | - Download provisioning profiles 60 | - Team selection 61 | - `https://itunesconnect.apple.com`: 62 | - Managing apps 63 | - Managing beta testers 64 | - Submitting updates to review 65 | - Managing app metadata 66 | - `https://du-itc.itunesconnect.apple.com`: 67 | - Upload icons, screenshots, trailers ... 68 | 69 | 70 | ## Contributing 71 | 72 | To get things running locally after cloning the repo: 73 | 74 | ``` 75 | $ swift package --enable-prefetching generate-xcodeproj 76 | $ open Shuttle.xcodeproj 77 | ``` 78 | 79 | If you want to be able to run the [Sources/Development/main.swift](Sources/Development/main.swift) file to test changes you just need to switch to use the `Development` scheme in Xcode and then edit the scheme settings and add two environment variables for `USERNAME` and `PASSWORD` (don't worry the Xcode project is in the gitignore so you won't accidently push up your credentials to the repo) 80 | 81 | -------------------------------------------------------------------------------- /Shuttle.md: -------------------------------------------------------------------------------- 1 | # Shuttle 2 | 3 | 4 | 5 | # ShuttleFramework -------------------------------------------------------------------------------- /Sources/DevPortal/DevPortal+API.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ShuttleCore 3 | import Moya 4 | import MoyaSugar 5 | 6 | public enum Platform: String, Codable { 7 | case mac 8 | case ios 9 | } 10 | 11 | enum DevPortalAPI { 12 | case teams 13 | } 14 | 15 | extension DevPortalAPI: SugarTargetType { 16 | var baseURL: URL { 17 | return DevPortalClient.hostname 18 | } 19 | 20 | var route: Route { 21 | switch self { 22 | case .teams: 23 | return .post("account/listTeams.action") 24 | } 25 | } 26 | 27 | var parameters: Parameters? { 28 | return nil 29 | } 30 | 31 | var headers: [String : String]? { 32 | return nil 33 | } 34 | 35 | var decodeKeyPath: String? { 36 | switch self { 37 | case .teams: return "teams" 38 | } 39 | } 40 | 41 | var validate: Bool { return true } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Sources/DevPortal/DevPortal+Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ShuttleCore 3 | import Moya 4 | 5 | public final class DevPortalClient: ShuttleCore.Client { 6 | public override class var hostname: URL { 7 | return URL(string: "https://developer.apple.com/services-account/\(protocolVersion)/")! 8 | } 9 | public static let headers: [String: String] = [ 10 | "Accept": "application/json", 11 | "Content-Type": "application/x-www-form-urlencoded", 12 | "User-Agent": userAgent, 13 | ] 14 | public override lazy var teams: [Team] = try! provider.requestSync(MultiTarget(DevPortalAPI.teams)).map([Team].self, atKeyPath: "teams") 15 | public override var teamId: String { 16 | get { 17 | if let currentId = currentTeamId { 18 | return currentId 19 | } 20 | if teams.count > 1 { 21 | print("The current user is in \(teams.count) teams. Pass a team ID or call `selectTeam` to choose a team. Using the first one for now.") 22 | } 23 | 24 | if teams.count == 0 { 25 | // TODO: Keep track of email 26 | let email = "Unknown" 27 | fatalError("User '\(email)' does not have access to any teams with an active membership") 28 | } 29 | currentTeamId = teams[0].id 30 | return currentTeamId! 31 | } 32 | set { 33 | guard teams.contains(where: { $0.id == newValue }) else { 34 | fatalError("That teamId doesn't exist for the authenticated account") 35 | } 36 | currentTeamId = newValue 37 | } 38 | } 39 | 40 | public class func login(email: String, password: String) -> DevPortalClient { 41 | let client = DevPortalClient() 42 | client.sendSharedLoginRequest(email: email, password: password) 43 | return client 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DevPortal/DevPortal.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | 3 | public struct DevPortal: Base { 4 | public static var _client: DevPortalClient? 5 | public static var client: DevPortalClient! 6 | 7 | // MARK: - Login 8 | 9 | public static func login(username: String, password: String) throws { 10 | client = DevPortalClient.login(email: username, password: password) 11 | } 12 | 13 | public static func selectTeam(id: String? = nil) throws { 14 | try client.selectTeam(id: id) 15 | } 16 | 17 | // MARK: - Apps 18 | 19 | public static var app: App.Type { 20 | let appType = App.self 21 | appType.client = client 22 | return appType 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DevPortal/DevPortalBase.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | 3 | //public class DevPortalBase { 4 | // 5 | // init(client: Client) { 6 | // 7 | // } 8 | //} 9 | 10 | -------------------------------------------------------------------------------- /Sources/DevPortal/Models/App.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | import Foundation 3 | import MoyaSugar 4 | import Moya 5 | 6 | enum BundleIdType: String, Codable { 7 | case wildcard 8 | case explicit 9 | } 10 | 11 | public struct CreateAppParams: Codable { 12 | let type: BundleIdType 13 | let name: String 14 | let bundleId: String 15 | let enableServices: [AppService] 16 | } 17 | 18 | enum AppAPI { 19 | case getAll(Platform, teamId: String) 20 | case create(Platform, teamId: String, params: CreateAppParams) 21 | case delete(Platform, teamId: String, appId: String) 22 | case update(Platform, teamId: String, name: String, appId: String) 23 | } 24 | 25 | extension AppAPI: ShuttleTargetType { 26 | typealias ResultType = App 27 | 28 | var baseURL: URL { 29 | return DevPortalClient.hostname 30 | } 31 | 32 | var route: Route { 33 | switch self { 34 | case .getAll(let platform, _): 35 | return .post("account/\(platform.rawValue)/identifiers/listAppIds.action") 36 | case .create(let platform, _, _): 37 | return .post("aacount/\(platform.rawValue)/identifiers/addAppId.action") 38 | case .delete(let platform, _, _): 39 | return .post("account/\(platform.rawValue)/identifiers/deleteAppId.action") 40 | case .update(let platform,_, _, _): 41 | return .post("account/\(platform.rawValue)/identifiers/updateAppIdName.action") 42 | } 43 | } 44 | 45 | var parameters: Parameters? { 46 | switch self { 47 | case let .getAll(_, teamId): 48 | return URLEncoding() => [ 49 | "teamId": teamId, 50 | "pageNumber": 1, 51 | "pageSize": 40, 52 | "sort": "name=asc", 53 | ] 54 | case let .create(_, teamId, params): 55 | var parameters = URLEncoding() => [ 56 | "teamId": teamId, 57 | "type": params.type.rawValue, 58 | "identifier": params.bundleId, 59 | "name": params.name, 60 | "push": params.type == .wildcard ? nil : "on", 61 | "inAppPurchase": params.type == .wildcard ? nil : "on", 62 | "gameCenter": params.type == .wildcard ? nil : "on", 63 | ] 64 | params.enableServices.forEach { service in 65 | parameters.values[service.serviceId] = service.values 66 | } 67 | return parameters 68 | case let .delete(_, teamId, appId): 69 | return URLEncoding() => [ 70 | "teamId": teamId, 71 | "appIdId": appId, 72 | ] 73 | case let .update(_, teamId, name, appId): 74 | return URLEncoding() => [ 75 | "teamId": teamId, 76 | "appIdId": appId, 77 | "name": name 78 | ] 79 | } 80 | } 81 | 82 | var headers: [String : String]? { 83 | let headers = DevPortalClient.headers 84 | // headers["csrf"] = "" 85 | // headers["csrf_ts"] = "" 86 | return headers 87 | } 88 | 89 | var decodeKeyPath: String? { 90 | switch self { 91 | case .getAll: return "appIds" 92 | case .create: return "appId" 93 | case .update: return "appId" 94 | default: return nil 95 | } 96 | } 97 | } 98 | 99 | public class App: Base, Codable { 100 | let appIdId: String 101 | public var appId: String { return appIdId } 102 | public let name: String 103 | let appIdPlatform: String 104 | public var platform: Platform { return Platform(rawValue: appIdPlatform)! } 105 | public let prefix: String 106 | let identifier: String 107 | public var bundleId: String { return identifier } 108 | public let isWildCard: Bool 109 | // public let features: [Feature] 110 | public let enabledFeatures: [String] 111 | public let isDevPushEnabled: Bool? 112 | public let isProdPushEnabled: Bool? 113 | let associatedApplicationGroupsCount: Int? 114 | public var appGroupsCount: Int { return associatedApplicationGroupsCount ?? 0} 115 | let associatedCloudContainersCount: Int? 116 | public var CloudContainersCount: Int { return associatedCloudContainersCount ?? 0 } 117 | let associatedIdentifiersCount: Int? 118 | public var identifiersCount: Int { return associatedIdentifiersCount ?? 0 } 119 | let associatedApplicationGroups: [AppGroup]? 120 | public var associatedGroups: [AppGroup] { return associatedApplicationGroups ?? [] } 121 | 122 | 123 | // MARK: - Static 124 | 125 | public static var _client: DevPortalClient? 126 | public static var client: DevPortalClient! 127 | 128 | public static func all(platform: Platform = .ios) throws -> [App] { 129 | return try client.provider.requestSyncDecodedArray(AppAPI.getAll(platform, teamId: client.teamId)) 130 | } 131 | 132 | public static func find(bundleId: String, platform: Platform = .ios) throws -> App? { 133 | return nil 134 | } 135 | 136 | public static func create(bundleId: String, name: String, platform: Platform = .ios, enableServices: [AppService] = []) throws -> App { 137 | let type: BundleIdType = bundleId.hasSuffix("*") ? .wildcard : .explicit 138 | let params = CreateAppParams(type: type, name: name, bundleId: bundleId, enableServices: enableServices) 139 | return try client.provider.requestSyncDecodedValue(AppAPI.create(platform, teamId: client.teamId, params: params)) 140 | } 141 | 142 | // MARK: - Instance 143 | 144 | public func delete() throws -> App { 145 | try App.client.provider.requestSync(AppAPI.delete(platform, teamId: App.client.teamId, appId: appId)) 146 | return self 147 | } 148 | 149 | public func update(name: String) throws -> App? { 150 | return try App.client.provider.requestSyncDecodedValue(AppAPI.update(platform, teamId: App.client.teamId, name: name, appId: appId)) 151 | } 152 | } 153 | 154 | -------------------------------------------------------------------------------- /Sources/DevPortal/Models/AppGroup.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct AppGroup: Codable { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /Sources/DevPortal/Models/AppService.swift: -------------------------------------------------------------------------------- 1 | 2 | public enum AppService: String, Codable { 3 | case appGroup = "app_group" 4 | case applePay = "apple_pay" 5 | case associatedDomains = "associated_domains" 6 | case dataProtection = "data_protection" 7 | case gameCenter = "game_center" 8 | case healthKit = "health_kit" 9 | case homeKit = "home_kit" 10 | case wirelessAccessory = "wireless_accessory" 11 | case cloud = "cloud" 12 | case cloudKit = "cloud_kit" 13 | case inAppPurchase = "in_app_purchase" 14 | case interAppAudio = "inter_app_audio" 15 | case passbook = "passbook" 16 | case pushNotification = "push_notification" 17 | case siriKit = "siri_kit" 18 | case vpnConfiguration = "vpn_configuration" 19 | case networkExtension = "network_extension" 20 | case hotspot = "hotspot" 21 | case multipath = "multipath" 22 | case nfcTagReading = "nfc_tag_reading" 23 | 24 | public var serviceId: String { 25 | switch self { 26 | case .appGroup: return "APG3427HIY" 27 | case .applePay: return "OM633U5T5G" 28 | case .associatedDomains: return "SKC3T5S89Y" 29 | case .dataProtection: return "dataProtection" 30 | case .gameCenter: return "gameCenter" 31 | case .healthKit: return "HK421J6T7P" 32 | case .homeKit: return "homeKit" 33 | case .wirelessAccessory: return "WC421J6T7P" 34 | case .cloud: return "iCloud" 35 | case .cloudKit: return "cloudKitVersion" 36 | case .inAppPurchase: return "inAppPurchase" 37 | case .interAppAudio: return "IAD53UNK2F" 38 | case .passbook: return "pass" 39 | case .pushNotification: return "push" 40 | case .siriKit: return "SI015DKUHP" 41 | case .vpnConfiguration: return "V66P55NK2I" 42 | case .networkExtension: return "NWEXT04537" 43 | case .hotspot: return "HSC639VEI8" 44 | case .multipath: return "MP49FN762P" 45 | case .nfcTagReading: return "NFCTRMAY17" 46 | } 47 | } 48 | 49 | public var serviceURI: String { 50 | if serviceId == "push" { 51 | return "account/ios/identifiers/updatePushService.action" 52 | } else { 53 | return "account/ios/identifiers/updateService.action" 54 | } 55 | } 56 | 57 | public var values: [String: Any]? { 58 | switch self { 59 | case .dataProtection: 60 | return [ 61 | "off": "", 62 | "complete": "complete", 63 | "unless_open": "unlessopen", 64 | "until_first_auth": "untilfirstauth" 65 | ] 66 | case .cloudKit: 67 | return [ 68 | "xcode5_compatible": 1, 69 | "cloud_kit": 2 70 | ] 71 | default: 72 | return nil 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Certificate.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Certificate.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Client.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Client.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Device.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Device.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Invite.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Invite.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Key.swift: -------------------------------------------------------------------------------- 1 | 2 | enum KeyAPI { 3 | case getKey(id: String, teamId: String) 4 | case downloadKey(id: String, teamId: String) 5 | case createKey(teamId: String, params: [String: String]) 6 | case revokeKey(id: String, teamId: String) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Merchant.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Merchant.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Passbook.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Passbook.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/Person.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/DevPortal/Models/Person.swift -------------------------------------------------------------------------------- /Sources/DevPortal/Models/ProvisioningProfile.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ShuttleCore 3 | import Moya 4 | import MoyaSugar 5 | 6 | enum ProvisioningProfileAPI { 7 | case getProvisioningProfiles(Platform, teamId: String) 8 | } 9 | 10 | extension ProvisioningProfileAPI: ShuttleTargetType { 11 | typealias ResultType = ProvisioningProfile 12 | 13 | var baseURL: URL { 14 | return DevPortalClient.hostname 15 | } 16 | 17 | var route: Route { 18 | switch self { 19 | case .getProvisioningProfiles(let platform, _): 20 | return .post("/account/\(platform.rawValue)/profile/listProvisioningProfiles.action") 21 | } 22 | } 23 | 24 | var parameters: Parameters? { 25 | return nil 26 | } 27 | 28 | var headers: [String : String]? { 29 | return nil 30 | } 31 | } 32 | 33 | struct ProvisioningProfile: Codable { 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Shuttle/ProvisioningProfile.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Sources/Shuttle/Shuttle.swift: -------------------------------------------------------------------------------- 1 | @_exported import TestFlight 2 | @_exported import Tunes 3 | @_exported import DevPortal 4 | 5 | //public struct Shuttle { 6 | // public static var text = "Hello, World!" 7 | //} 8 | 9 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/AlamofireDecodableError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlamofireDecodableError.swift 3 | // CodableAlamofire 4 | // 5 | // Created by Nikita Ermolenko on 11/06/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `AlamofireDecodableError` is the error type returned by CodableAlamofire. 11 | /// 12 | /// - invalidKeyPath: Returned when a nested dictionary object doesn't exist by special keyPath. 13 | /// - emptyKeyPath: Returned when a keyPath is empty. 14 | 15 | public enum AlamofireDecodableError: Error { 16 | case invalidKeyPath 17 | case emptyKeyPath 18 | } 19 | 20 | extension AlamofireDecodableError: LocalizedError { 21 | 22 | public var errorDescription: String? { 23 | switch self { 24 | case .invalidKeyPath: return "Nested object doesn't exist by this keyPath." 25 | case .emptyKeyPath: return "KeyPath can not be empty." 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/AppleAuth+API.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Moya 3 | import MoyaSugar 4 | 5 | public enum AppleAuthAPI { 6 | case signIn(email: String, password: String, cookie: String?) 7 | case twoStepAuth 8 | case securityCode(String) 9 | case selectDevice(deviceId: String) 10 | case trust 11 | } 12 | 13 | extension AppleAuthAPI: SugarTargetType { 14 | public var baseURL: URL { 15 | return URL(string: "https://idmsa.apple.com/appleauth/auth")! 16 | } 17 | 18 | public var route: Route { 19 | switch self { 20 | case .signIn: 21 | return .post("/signin") 22 | case .twoStepAuth: 23 | return .get("") 24 | case .securityCode: 25 | return .post("/verify/trusteddevice/securitycode") 26 | case .selectDevice(let deviceId): 27 | return .put("/verify/device/\(deviceId)/securitycode") 28 | case .trust: 29 | return .get("/2sv/trust") 30 | } 31 | } 32 | 33 | public var parameters: Parameters? { 34 | switch self { 35 | case .signIn(let email, let password, _): 36 | return JSONEncoding() => [ 37 | "accountName": email, 38 | "password": password, 39 | "rememberMe": true, 40 | ] 41 | case .securityCode(let code): 42 | return JSONEncoding() => [ 43 | "securityCode": [ 44 | "code": code 45 | ] 46 | ] 47 | default: 48 | return nil 49 | } 50 | } 51 | 52 | public var headers: [String: String]? { 53 | var headers = [String: String]() 54 | headers["Content-Type"] = "application/json" 55 | headers["X-Apple-Id-Session-Id"] = Client.sessionId 56 | headers["X-Apple-Widget-Key"] = Client.itcServiceKey 57 | headers["Accept"] = "application/json" 58 | headers["Scnt"] = Client.scnt 59 | 60 | switch self { 61 | case .signIn(_, _, let cookie): 62 | headers["X-Requested-With"] = "XMLHttpRequest" 63 | if let c = cookie { 64 | headers["Cookie"] = c 65 | } 66 | return headers 67 | default: 68 | break 69 | } 70 | return headers 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Base.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | public protocol Base { 4 | associatedtype ClientType: Client 5 | 6 | static var _client: ClientType? { get set } 7 | static var client: ClientType! { get } 8 | } 9 | 10 | extension Base { 11 | static var client: ClientType! { 12 | get { 13 | guard let client = _client else { 14 | fatalError("Please login using `Spaceship.DevPortal.login(username: \"user\", \"password\")`") 15 | } 16 | return client 17 | } 18 | set { 19 | _client = newValue 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Client+TwoStepAuth.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Moya 3 | 4 | struct TwoStepAuthResponse: Codable { 5 | let trustedDevices: [String] 6 | } 7 | 8 | struct TwoFactorAuthResponse: Codable { 9 | let trustedPhoneNumbers: [TwoFactorPhoneNumber] 10 | let trustedPhoneNumber: TwoFactorPhoneNumber 11 | let securityCode: SecurityCode 12 | } 13 | struct TwoFactorPhoneNumber: Codable { 14 | let id: Int 15 | let obfuscatedNumber: String 16 | } 17 | 18 | struct SecurityCode: Codable { 19 | let length: Int 20 | let tooManyCodesSent: Bool 21 | let tooManyCodesValidated: Bool 22 | let securityCodeLocked: Bool 23 | } 24 | 25 | extension Client { 26 | func handleTwoStep(response: Response) { 27 | guard let appleIdSessionId = response.response?.allHeaderFields["X-Apple-ID-Session-Id"] as? String, 28 | let scnt = response.response?.allHeaderFields["scnt"] as? String else { 29 | fatalError("Failed to required two step headers") 30 | } 31 | Client.sessionId = appleIdSessionId 32 | Client.scnt = scnt 33 | 34 | let res = try! appleAuthProvider.requestSync(.twoStepAuth) 35 | 36 | // guard res.statusCode == 200 else { 37 | // fatalError("Failed to get two factor code info: \(res.response)") 38 | // } 39 | 40 | if let _ = try? res.map(TwoStepAuthResponse.self) { 41 | // TODO: Handle two step 42 | } else if let twoFactorResponse = try? res.map(TwoFactorAuthResponse.self) { 43 | handleTwoFactor(response: twoFactorResponse) 44 | } else { 45 | fatalError("Invalid 2 step response: \(res)") 46 | } 47 | } 48 | 49 | func handleTwoFactor(response: TwoFactorAuthResponse) { 50 | let twoFactorUrl = "" 51 | print("Two Factor Authentication for account '\(userEmail ?? "???")' is enabled") 52 | 53 | let cookiePersisted = false 54 | let shuttleSessionEnv = "" 55 | if !cookiePersisted && shuttleSessionEnv.isEmpty { 56 | print("If you're running this in a non-interactive session (e.g. server or CI)") 57 | print("check out \(twoFactorUrl)") 58 | } else { 59 | // If the cookie is set but still required, the cookie is expired 60 | print("Your session cookie has been expired.") 61 | } 62 | 63 | let codeLength = response.securityCode.length 64 | print("Please enter the \(codeLength) digit code: ") 65 | guard let code = readLine() else { 66 | fatalError("Failed to get code from user") 67 | } 68 | print("Requesting session...") 69 | 70 | // Send securityCode back to server to get a valid session 71 | do { 72 | _ = try appleAuthProvider.requestSync(.securityCode(code)) 73 | 74 | // we use `TunesClient.handleITCResponse` 75 | // since this might be from the Dev Portal, but for 2 step 76 | // TunesClient.handleITCResponse(r.body) 77 | 78 | storeSession() 79 | } catch let error { 80 | fatalError("Failed to get session: \(error.localizedDescription)") 81 | } 82 | 83 | } 84 | 85 | // Only needed for 2 step 86 | func loadSessionFromFile() -> String? { 87 | return "" 88 | } 89 | 90 | func loadSessionFromEnv() -> String? { 91 | return "" 92 | } 93 | 94 | // Fetch the session cookie from the environment 95 | // (if exists) 96 | static var shuttleSessionEnv: String? { 97 | return "" 98 | } 99 | 100 | func selectDevice(id: String) throws { 101 | 102 | } 103 | 104 | func storeSession() { 105 | // If the request was successful, r.body is actually nil 106 | // The previous request will fail if the user isn't on a team 107 | // on iTunes Connect, but it still works, so we're good 108 | 109 | // Tell iTC that we are trustworthy (obviously) 110 | // This will update our local cookies to something new 111 | // They probably have a longer time to live than the other poor cookies 112 | // Changed Keys 113 | // - myacinfo 114 | // - DES5c148586dfd451e55afb0175f62418f91 115 | // We actually only care about the DES value 116 | 117 | _ = try? appleAuthProvider.requestSync(.trust) 118 | 119 | // This request will fail if the user isn't added to a team on iTC 120 | // However we don't really care, this request will still return the 121 | // correct DES... cookie 122 | 123 | storeCookie() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Alamofire 3 | import Moya 4 | 5 | //protocol BasicPreferredInfoError { 6 | // var preferredInfo: String { get } 7 | //} 8 | 9 | enum BasicPreferredInfoError: Swift.Error { 10 | case invalidUserCredentials 11 | case noUserCredentials 12 | case programLicenseAgreementUpdated 13 | case insufficientPermissions 14 | case unexpectedResponse 15 | case appleTimeoutError 16 | case unauthorizedAccessError 17 | case internalServerError 18 | 19 | // TODO: Implement 20 | var showGitHubIssues: Bool { 21 | return false 22 | } 23 | 24 | var preferredInfo: String? { 25 | switch self { 26 | case .insufficientPermissions: 27 | return "Insufficient permissions for your Apple ID: " 28 | case .unexpectedResponse: 29 | return "Apple provided the following error info: " 30 | default: 31 | return "The request could not be completed because: " 32 | } 33 | } 34 | } 35 | 36 | enum ITunesConnectError: Swift.Error { 37 | case generic(String) 38 | case temporaryError 39 | case potentialServerError 40 | } 41 | 42 | 43 | public protocol AuthenticatedClient { 44 | func sendLoginRequest(email: String, password: String) 45 | } 46 | 47 | private func JSONResponseDataFormatter(_ data: Data) -> Data { 48 | do { 49 | let dataAsJSON = try JSONSerialization.jsonObject(with: data) 50 | let prettyData = try JSONSerialization.data(withJSONObject: dataAsJSON, options: .prettyPrinted) 51 | return prettyData 52 | } catch { 53 | return data // fallback to original data if it can't be serialized. 54 | } 55 | } 56 | 57 | open class Client { 58 | let plugins: [PluginType] = [ 59 | NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter), 60 | RequestCSRFPlugin() 61 | ] 62 | open lazy var provider = MoyaProvider(plugins: plugins) 63 | lazy var olympusProvider = MoyaProvider() 64 | lazy var appleAuthProvider = MoyaProvider() 65 | lazy var tunesCoreProvider = MoyaProvider() 66 | 67 | open class var hostname: URL { 68 | fatalError("You must implement self.hostname") 69 | } 70 | public static let protocolVersion: String = "QH65B2" 71 | public static let userAgent: String = "Shuttle 1.0" 72 | static var sessionId: String? = nil 73 | static var scnt: String? = nil 74 | 75 | public var user: User? = nil 76 | public var userEmail: String? { 77 | return user?.emailAddress 78 | } 79 | // var csrfTokens: [String] { get set } 80 | 81 | open lazy var userDetails = try! tunesCoreProvider.requestSync(.userDetails).map(UserDetailsData.self) 82 | open lazy var teams: [Team] = userDetails.associatedAccounts.map { Team(name: $0.contentProvider.name, teamId: String($0.contentProvider.contentProviderId)) } 83 | 84 | public var currentTeamId: String? = nil 85 | open var teamId: String { 86 | get { 87 | if let currentId = currentTeamId { 88 | return currentId 89 | } 90 | if teams.count > 1 { 91 | print("The current user is in \(teams.count) teams. Pass a team ID or call `selectTeam` to choose a team. Using the first one for now.") 92 | } 93 | currentTeamId = teams[0].id 94 | return currentTeamId! 95 | } 96 | set { 97 | guard teams.contains(where: { $0.id == newValue }) else { 98 | fatalError("Could not set team ID to '\(newValue)', only found the following available teams:\n\n\(teams.map { "- \($0.id) (\($0.name)" }.joined(separator: "\n"))\n") 99 | } 100 | 101 | 102 | 103 | currentTeamId = newValue 104 | } 105 | } 106 | 107 | public func selectTeam(id: String? = nil) throws { 108 | if let id = id { 109 | print("Attempting to select team with id: `\(id)`") 110 | teamId = id 111 | } else if teams.count > 1 { 112 | print("Multiple teams found on the " + "Developer Portal".yellow + ", please enter the number of the team you want to use:") 113 | for (id, team) in teams.enumerated() { 114 | print("\(id + 1). \(team.name) (\(team.id))") 115 | } 116 | print("> ", terminator: "") 117 | if let indexString = readLine(), let index = Int(indexString) { 118 | print("Selecting team `\(teams[index].name)`...") 119 | teamId = teams[index].id 120 | } else { 121 | fatalError("Failed to get teamId") 122 | } 123 | } else { 124 | print("Only one team available") 125 | } 126 | } 127 | 128 | // static func clientWithAuth(from client: Client) -> Self 129 | 130 | public required init(cookie: String? = nil, teamId: String? = nil) { 131 | self.currentTeamId = teamId 132 | } 133 | 134 | // MARK: - Paging 135 | 136 | // The page size we want to request, defaults to 500 137 | public let pageSize: Int = 40 138 | 139 | // Handles the paging for you... for free 140 | // Just pass a block and use the parameter as page number 141 | public func paging(_ block: (Int) -> [Result]?) -> [Result] { 142 | return [] 143 | } 144 | 145 | // MARK: - Login and Team Selection 146 | 147 | public func sendSharedLoginRequest(email: String, password: String) { 148 | let response = try! appleAuthProvider.requestSync(.signIn(email: email, password: password, cookie: nil)) 149 | // print("Auth Response: \(String(data: response.data, encoding: .utf8)!)") 150 | switch response.statusCode { 151 | case 403: 152 | print("Invalid username and password combination. Used '\(email)' as the username.") 153 | case 200: 154 | // We are good to go fetch session now 155 | fetchOlympusSession() 156 | case 409: 157 | // 2 factor is enabled for this account, first handle that 158 | // and then get the olympus session 159 | print("Two factor is enabled for this account and isn't supported yet") 160 | handleTwoStep(response: response) 161 | fetchOlympusSession() 162 | default: 163 | // Need to handle other cases still 164 | print("Invalid username and password combination. Used '\(email)' as the username.") 165 | } 166 | } 167 | 168 | func fetchOlympusSession() { 169 | _ = try! olympusProvider.requestSync(.session).map(OlympusSessionResponse.self) 170 | // TODO: Track providers 171 | // teams = sessionResponse.availableProviders 172 | // print(sessionResponse) 173 | } 174 | 175 | static var itcServiceKey = { 176 | return try! MoyaProvider().requestSync(.itcServiceKey).map(AuthService.self).authServiceKey 177 | }() 178 | 179 | var cookie: String? = nil 180 | 181 | func storeCookie() { 182 | 183 | } 184 | 185 | var autobahnUserDir: URL { 186 | return URL(fileURLWithPath: "") 187 | } 188 | 189 | var persistentCookieURL: URL { 190 | // if ENV["SPACESHIP_COOKIE_PATH"] 191 | // path = File.expand_path(File.join(ENV["SPACESHIP_COOKIE_PATH"], "spaceship", self.user, "cookie")) 192 | // else 193 | // [File.join(self.fastlane_user_dir, "spaceship"), "~/.spaceship", "/var/tmp/spaceship", "#{Dir.tmpdir}/spaceship"].each do |dir| 194 | // dir_parts = File.split(dir) 195 | // if directory_accessible?(File.expand_path(dir_parts.first)) 196 | // path = File.expand_path(File.join(dir, self.user, "cookie")) 197 | // break 198 | // end 199 | // end 200 | return URL(fileURLWithPath: "") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Core.swift: -------------------------------------------------------------------------------- 1 | @_exported import Rainbow 2 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Extensions/DataRequest+Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataRequest+Decodable.swift 3 | // CodableAlamofire 4 | // 5 | // Created by Nikita Ermolenko on 10/06/2017. 6 | // Copyright © 2017 Nikita Ermolenko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public extension DataRequest { 13 | 14 | private static func DecodableObjectSerializer(_ keyPath: String?, _ decoder: JSONDecoder) -> DataResponseSerializer { 15 | return DataResponseSerializer { _, response, data, error in 16 | if let error = error { 17 | return .failure(error) 18 | } 19 | if let keyPath = keyPath { 20 | if keyPath.isEmpty { 21 | return .failure(AlamofireDecodableError.emptyKeyPath) 22 | } 23 | return DataRequest.decodeToObject(byKeyPath: keyPath, decoder: decoder, response: response, data: data) 24 | } 25 | return DataRequest.decodeToObject(decoder: decoder, response: response, data: data) 26 | } 27 | } 28 | 29 | private static func decodeToObject(decoder: JSONDecoder, response: HTTPURLResponse?, data: Data?) -> Result { 30 | let result = Request.serializeResponseData(response: response, data: data, error: nil) 31 | 32 | switch result { 33 | case .success(let data): 34 | do { 35 | let object = try decoder.decode(T.self, from: data) 36 | return .success(object) 37 | } 38 | catch { 39 | return .failure(error) 40 | } 41 | case .failure(let error): return .failure(error) 42 | } 43 | } 44 | 45 | private static func decodeToObject(byKeyPath keyPath: String, decoder: JSONDecoder, response: HTTPURLResponse?, data: Data?) -> Result { 46 | let result = Request.serializeResponseJSON(options: [], response: response, data: data, error: nil) 47 | 48 | switch result { 49 | case .success(let json): 50 | if let nestedJson = (json as AnyObject).value(forKeyPath: keyPath) { 51 | do { 52 | let data = try JSONSerialization.data(withJSONObject: nestedJson) 53 | let object = try decoder.decode(T.self, from: data) 54 | return .success(object) 55 | } 56 | catch { 57 | return .failure(error) 58 | } 59 | } 60 | else { 61 | return .failure(AlamofireDecodableError.invalidKeyPath) 62 | } 63 | case .failure(let error): return .failure(error) 64 | } 65 | } 66 | 67 | 68 | /// Adds a handler to be called once the request has finished. 69 | 70 | /// - parameter queue: The queue on which the completion handler is dispatched. 71 | /// - parameter keyPath: The keyPath where object decoding should be performed. Default: `nil`. 72 | /// - parameter decoder: The decoder that performs the decoding of JSON into semantic `Decodable` type. Default: `JSONDecoder()`. 73 | /// - parameter completionHandler: The code to be executed once the request has finished and the data has been mapped by `JSONDecoder`. 74 | 75 | /// - returns: The request. 76 | 77 | @discardableResult 78 | public func responseDecodableObject(queue: DispatchQueue? = nil, keyPath: String? = nil, decoder: JSONDecoder = JSONDecoder(), completionHandler: @escaping (DataResponse) -> Void) -> Self { 79 | return response(queue: queue, responseSerializer: DataRequest.DecodableObjectSerializer(keyPath, decoder), completionHandler: completionHandler) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Extensions/MoyaProvider+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Moya 3 | import Result 4 | 5 | public extension MoyaProvider where Target == MultiTarget { 6 | @discardableResult 7 | public func requestSyncDecodedValue(_ target: T) throws -> T.ResultType { 8 | do { 9 | let response = try requestSync(MultiTarget(target)).map(T.ResultType.self, atKeyPath: target.decodeKeyPath) 10 | return response 11 | } catch MoyaError.jsonMapping(let response) { 12 | print("Failed to decode `\(String(describing: T.ResultType.self))` at keyPath `\(target.decodeKeyPath ?? "")` from response: \n\(String(data: response.data, encoding: .utf8)!)") 13 | throw MoyaError.jsonMapping(response) 14 | } 15 | } 16 | 17 | @discardableResult 18 | public func requestSyncDecodedArray(_ target: T) throws -> [T.ResultType] { 19 | do { 20 | let response = try requestSync(MultiTarget(target)).map([T.ResultType].self, atKeyPath: target.decodeKeyPath) 21 | return response 22 | } catch MoyaError.jsonMapping(let response) { 23 | print("Failed to decode `\(String(describing: T.ResultType.self))` at keyPath `\(target.decodeKeyPath ?? "")` from response: \n\(String(data: response.data, encoding: .utf8)!)") 24 | throw MoyaError.jsonMapping(response) 25 | } 26 | } 27 | 28 | @discardableResult 29 | public func requestSync(_ target: T) throws -> Moya.Response { 30 | return try requestSync(MultiTarget(target)) 31 | } 32 | } 33 | 34 | extension MoyaProvider { 35 | @discardableResult 36 | public func requestSync(_ target: Target) throws -> Moya.Response { 37 | let semaphore = DispatchSemaphore(value: 0) 38 | var response: Moya.Response? = nil 39 | var error: Error? = nil 40 | request(target, callbackQueue: .global(qos: .background)) { (result: Result) in 41 | defer { semaphore.signal() } 42 | switch result { 43 | case .success(let res): 44 | response = res 45 | case .failure(let err): 46 | error = err 47 | } 48 | } 49 | semaphore.wait() 50 | 51 | guard error == nil else { 52 | throw error! 53 | } 54 | return response! 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Extensions/MoyaSugar+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Moya 3 | import MoyaSugar 4 | 5 | extension SugarTargetType { 6 | public var sampleData: Data { 7 | return Data() 8 | } 9 | } 10 | 11 | public protocol ShuttleTargetType: SugarTargetType { 12 | associatedtype ResultType: Decodable 13 | var decodeKeyPath: String? { get } 14 | } 15 | 16 | public extension ShuttleTargetType { 17 | var decodeKeyPath: String? { return nil } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Extensions/RequestCSRFPlugin.swift: -------------------------------------------------------------------------------- 1 | import Moya 2 | import Result 3 | import Foundation 4 | 5 | final class RequestCSRFPlugin: PluginType { 6 | 7 | var csrfToken: String? 8 | var csrfTimestamp: String? 9 | 10 | func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { 11 | guard let csrf = csrfToken, 12 | let csrfTs = csrfTimestamp else { 13 | return request 14 | } 15 | var request = request 16 | request.addValue(csrf, forHTTPHeaderField: "csrf") 17 | request.addValue(csrfTs, forHTTPHeaderField: "csrf_ts") 18 | return request 19 | } 20 | 21 | func didReceive(_ result: Result, target: TargetType) { 22 | switch result { 23 | case .success(let response): 24 | csrfToken = response.response?.allHeaderFields["csrf"] as? String 25 | csrfTimestamp = response.response?.allHeaderFields["csrf_ts"] as? String 26 | default: 27 | return 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Extensions/Response+Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Decodable.swift 3 | // Moya-Decodable 4 | // 5 | // Created by chanju Jeon on 2017. 6. 19.. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | public extension Response { 12 | 13 | public func mapObject() throws -> T { 14 | guard let jsonDictionary = try mapJSON() as? NSDictionary else { 15 | throw MoyaError.jsonMapping(self) 16 | } 17 | 18 | do { 19 | let data = try JSONSerialization.data(withJSONObject: jsonDictionary, options: .prettyPrinted) 20 | return try JSONDecoder().decode(T.self, from: data) 21 | } catch { 22 | throw MoyaError.jsonMapping(self) 23 | } 24 | } 25 | 26 | public func mapObject(withKeyPath keyPath: String?) throws -> T { 27 | guard let keyPath = keyPath else { 28 | return try mapObject() 29 | } 30 | 31 | guard let jsonDictionary = try mapJSON() as? NSDictionary, let objectDictionary = jsonDictionary.value(forKeyPath: keyPath) as? NSDictionary else { 32 | throw MoyaError.jsonMapping(self) 33 | } 34 | 35 | do { 36 | let data = try JSONSerialization.data(withJSONObject: objectDictionary, options: .prettyPrinted) 37 | return try JSONDecoder().decode(T.self, from: data) 38 | } catch { 39 | throw MoyaError.jsonMapping(self) 40 | } 41 | } 42 | 43 | public func mapArray() throws -> [T] { 44 | guard let jsonArray = try mapJSON() as? NSArray else { 45 | throw MoyaError.jsonMapping(self) 46 | } 47 | 48 | do { 49 | let data = try JSONSerialization.data(withJSONObject: jsonArray, options: .prettyPrinted) 50 | return try JSONDecoder().decode([T].self, from: data) 51 | } catch { 52 | throw MoyaError.jsonMapping(self) 53 | } 54 | } 55 | 56 | public func mapArray(withKeyPath keyPath: String?) throws -> [T] { 57 | guard let keyPath = keyPath else { 58 | return try mapArray() 59 | } 60 | 61 | guard let jsonDictionary = try mapJSON() as? NSDictionary, let objectArray = jsonDictionary.value(forKeyPath: keyPath) as? NSArray else { 62 | throw MoyaError.jsonMapping(self) 63 | } 64 | 65 | do { 66 | let data = try JSONSerialization.data(withJSONObject: objectArray, options: .prettyPrinted) 67 | return try JSONDecoder().decode([T].self, from: data) 68 | } catch { 69 | throw MoyaError.jsonMapping(self) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/AuthService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AuthService: Codable { 4 | let authServiceUrl: URL 5 | let authServiceKey: String 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/HelpLink.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HelpLink: Codable { 4 | let key: String 5 | let url: URL 6 | let localizedText: String 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/ITCModule.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ITCModule: Codable { 4 | let key: String 5 | let name: String 6 | let localizedName: String 7 | let url: URL 8 | let iconUrl: URL 9 | let down: Bool 10 | let hasNotifications: Bool 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/OlympusSessionResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct OlympusSessionResponse: Codable { 4 | let user: User 5 | let provider: OlympusContentProvider 6 | let availableProviders: [OlympusContentProvider] 7 | let backingType: String 8 | let roles: [Role] 9 | let unverifiedRoles: [Role] 10 | // let featureFlags: [String] 11 | let agreeToTerms: Bool 12 | let modules: [ITCModule] 13 | let helpLinks: [HelpLink] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/Team.swift: -------------------------------------------------------------------------------- 1 | public enum ContentType: String, Codable { 2 | case software = "SOFTWARE" 3 | } 4 | 5 | public struct OlympusContentProvider: Codable { 6 | var providerId: Int 7 | var name: String 8 | var contentTypes: [ContentType] 9 | } 10 | 11 | public struct ITunesConnectContentProvider: Codable { 12 | let contentProviderId: Int 13 | let name: String 14 | } 15 | 16 | public struct Team: Codable { 17 | public let name: String 18 | let teamId: String 19 | public var id: String { 20 | return teamId 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Role: String, Codable { 4 | case admin = "ADMIN" 5 | case appManager = "APP_MANAGER" 6 | case customerSupport = "CUSTOMER_SUPPORT" 7 | } 8 | 9 | public struct User: Codable { 10 | let fullName: String 11 | let emailAddress: String 12 | let prsId: String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Models/UserDetailsResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct UserDetailsData: Codable { 4 | let associatedAccounts: [AssociatedAccount] 5 | let contentProviderId: String 6 | let displayName: String 7 | let contentProvider: String 8 | let userName: String 9 | } 10 | 11 | public struct AssociatedAccount: Codable { 12 | let contentProvider: ITunesConnectContentProvider 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/Olympus+API.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Moya 3 | import MoyaSugar 4 | 5 | public enum OlympusAPI { 6 | case itcServiceKey 7 | case session 8 | } 9 | 10 | extension OlympusAPI: SugarTargetType { 11 | public var baseURL: URL { 12 | return URL(string: "https://olympus.itunes.apple.com/v1")! 13 | } 14 | 15 | public var route: Route { 16 | switch self { 17 | case .itcServiceKey: 18 | return .get("/app/config") 19 | case .session: 20 | return .get("/session") 21 | } 22 | } 23 | 24 | public var parameters: Parameters? { 25 | switch self { 26 | case .itcServiceKey: 27 | return URLEncoding() => [ 28 | "hostname": "itunesconnect.apple.com", 29 | ] 30 | default: 31 | return nil 32 | } 33 | } 34 | 35 | public var headers: [String : String]? { 36 | var headers = [String: String]() 37 | headers["Content-Type"] = "application/json" 38 | headers["X-Apple-Id-Session-Id"] = Client.sessionId 39 | // headers["X-Apple-Widget-Key"] = Client.itcServiceKey 40 | headers["Accept"] = "application/json" 41 | headers["Scnt"] = Client.scnt 42 | 43 | return headers 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ShuttleCore/TunesCore+API.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MoyaSugar 3 | import Moya 4 | 5 | public enum TunesCoreAPI { 6 | case userDetails 7 | case setTeamId(String, dsId: String) 8 | } 9 | 10 | extension TunesCoreAPI: SugarTargetType { 11 | public var baseURL: URL { 12 | return URL(string: "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa")! 13 | } 14 | 15 | public var route: Route { 16 | switch self { 17 | case .userDetails: return .get("/ra/user/details") 18 | case .setTeamId: return .post("/ra/v1/session/webSession") 19 | } 20 | } 21 | 22 | public var parameters: Parameters? { 23 | switch self { 24 | case let.setTeamId(id, dsId): 25 | return JSONEncoding() => [ 26 | "contentProviderId": id, 27 | "dsId": dsId 28 | ] 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | public var headers: [String : String]? { 35 | return [ 36 | "Content-Type": "application/json" 37 | ] 38 | } 39 | 40 | public var decodeKeyPath: String? { 41 | switch self { 42 | case .userDetails: return "data" 43 | default: return nil 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Sources/ShuttleDevelopment/main.swift: -------------------------------------------------------------------------------- 1 | import Shuttle 2 | import Foundation 3 | 4 | do { 5 | // MARK: - DevPortal API 6 | 7 | // MARK: - Login 8 | 9 | let env = ProcessInfo.processInfo.environment 10 | 11 | try Shuttle.DevPortal.login(username: env["USERNAME"]!, password: env["PASSWORD"]!) 12 | try Shuttle.DevPortal.selectTeam() 13 | 14 | // MARK: - Apps 15 | 16 | // Fetch all available apps 17 | _ = try Shuttle.DevPortal.app.all() 18 | 19 | // Find a specific app based on the bundle identifier 20 | _ = try Shuttle.DevPortal.app.find(bundleId: "com.kdawgwilk.app") 21 | 22 | // Show the names of all your apps 23 | try Shuttle.DevPortal.app.all().forEach { app in 24 | print(app.name) 25 | } 26 | 27 | // Create a new app 28 | //_ = try Shuttle.DevPortal.app.create(bundleId: "com.kdawgwilk.app_name", name: "Autobahn App") 29 | 30 | // MARK: - App Groups 31 | 32 | // Fetch all existing app groups 33 | //all_groups = Shuttle.DevPortal.app_group.all 34 | // 35 | // Find a specific app group, based on the identifier 36 | //group = Shuttle.DevPortal.app_group.find("group.com.example.application") 37 | // 38 | // Show the names of all the groups 39 | //Shuttle.DevPortal.app_group.all.collect do |group| 40 | //group.name 41 | //end 42 | // 43 | // Create a new group 44 | //group = Shuttle.DevPortal.app_group.create!(group_id: "group.com.example.another", 45 | //name: "Another group") 46 | // 47 | // Associate an app with this group (overwrites any previous associations) 48 | // Assumes app contains a fetched app, as described above 49 | //app = app.associate_groups([group]) 50 | 51 | // MARK: - Apple Pay Merchants 52 | 53 | // Fetch all existing merchants 54 | //all_merchants = Shuttle.DevPortal.merchant.all 55 | // 56 | // Find a specific merchant, based on the identifier 57 | //sandbox_merchant = Shuttle.DevPortal.merchant.find("merchant.com.example.application.sandbox") 58 | // 59 | // Show the names of all the merchants 60 | //Shuttle.DevPortal.merchant.all.collect do |merchant| 61 | //merchant.name 62 | //end 63 | // 64 | // Create a new merchant 65 | //another_merchant = Shuttle.DevPortal.merchant.create!(bundle_id: "merchant.com.example.another", name: "Another merchant") 66 | // 67 | // Delete a merchant 68 | //another_merchant.delete! 69 | // 70 | // Associate an app with merchant/s (overwrites any previous associations) 71 | // Assumes app contains a fetched app, as described above 72 | //app = app.associate_merchants([sandbox_merchant, production_merchant]) 73 | 74 | // MARK: - Passbook 75 | 76 | // Fetch all existing passbooks 77 | //all_passbooks = Shuttle.DevPortal.passbook.all 78 | // 79 | // Find a specific passbook, based on the identifier 80 | //passbook = Shuttle.DevPortal.passbook.find("pass.com.example.passbook") 81 | // 82 | // Create a new passbook 83 | //passbook = Shuttle.DevPortal.passbook.create!(bundle_id: 'pass.com.example.passbook', name: 'Fastlane Passbook') 84 | // 85 | // Delete a passbook using his identifier 86 | //passbook = Shuttle.DevPortal.passbook.find("pass.com.example.passbook").delete! 87 | 88 | // MARK: - Certificates 89 | 90 | // Fetch all available certificates (includes signing and push profiles) 91 | //certificates = Shuttle.DevPortal.certificate.all 92 | 93 | // Code Signing Certificates 94 | 95 | // Production identities 96 | //prod_certs = Shuttle.DevPortal.certificate.production.all 97 | // 98 | // Development identities 99 | //dev_certs = Shuttle.DevPortal.certificate.development.all 100 | // 101 | // Download a certificate 102 | //cert_content = prod_certs.first.download 103 | 104 | // Push Certificates 105 | 106 | // Production push profiles 107 | //prod_push_certs = Shuttle.DevPortal.certificate.production_push.all 108 | // 109 | // Development push profiles 110 | //dev_push_certs = Shuttle.DevPortal.certificate.development_push.all 111 | // 112 | // Download a push profile 113 | //cert_content = dev_push_certs.first.download 114 | // 115 | // Creating a push certificate 116 | // 117 | // Create a new certificate signing request 118 | //csr, pkey = Shuttle.DevPortal.certificate.create_certificate_signing_request 119 | // 120 | // Use the signing request to create a new push certificate 121 | //Shuttle.DevPortal.certificate.production_push.create!(csr: csr, bundle_id: "com.krausefx.app") 122 | 123 | // Create a Certificate 124 | 125 | // Create a new certificate signing request 126 | //csr, pkey = Shuttle.DevPortal.certificate.create_certificate_signing_request 127 | // 128 | // Use the signing request to create a new distribution certificate 129 | //Shuttle.DevPortal.certificate.production.create!(csr: csr) 130 | 131 | // MARK: - Provisioning Profiles 132 | 133 | // Recieving profiles 134 | 135 | // Finding 136 | // 137 | // Get all available provisioning profiles 138 | //profiles = Shuttle.DevPortal.provisioning_profile.all 139 | // 140 | // Get all App Store and Ad Hoc profiles 141 | // Both app_store.all and ad_hoc.all return the same 142 | // This is the case since September 2016, since the API has changed 143 | // and there is no fast way to get the type when fetching the profiles 144 | //profiles_appstore_adhoc = Shuttle.DevPortal.provisioning_profile.app_store.all 145 | //profiles_appstore_adhoc = Shuttle.DevPortal.provisioning_profile.ad_hoc.all 146 | // 147 | // To distinguish between App Store and Ad Hoc profiles use 148 | //adhoc_only = profiles_appstore_adhoc.find_all do |current_profile| 149 | //current_profile.is_adhoc? 150 | //end 151 | // 152 | // Get all Development profiles 153 | //profiles_dev = Shuttle.DevPortal.provisioning_profile.development.all 154 | // 155 | // Fetch all profiles for a specific app identifier for the App Store (Array of profiles) 156 | //filtered_profiles = Shuttle.DevPortal.provisioning_profile.app_store.find_by_bundle_id("com.krausefx.app") 157 | // 158 | // Check if a provisioning profile is valid 159 | //profile.valid? 160 | // 161 | // Verify that the certificate of the provisioning profile is valid 162 | //profile.certificate_valid? 163 | // 164 | // Downloading 165 | // 166 | // Download a profile 167 | //profile_content = profiles.first.download 168 | // 169 | // Download a specific profile as file 170 | //matching_profiles = Shuttle.DevPortal.provisioning_profile.app_store.find_by_bundle_id("com.krausefx.app") 171 | //first_profile = matching_profiles.first 172 | // 173 | //File.write("output.mobileprovision", first_profile.download) 174 | 175 | // Create a provisioning profile 176 | 177 | // Choose the certificate to use 178 | //cert = Shuttle.DevPortal.certificate.production.all.first 179 | // 180 | // Create a new provisioning profile with a default name 181 | // The name of the new profile is "com.krausefx.app AppStore" 182 | //profile = Shuttle.DevPortal.provisioning_profile.app_store.create!(bundle_id: "com.krausefx.app", 183 | //certificate: cert) 184 | // 185 | // AdHoc Profiles will add all devices by default 186 | //profile = Shuttle.DevPortal.provisioning_profile.ad_hoc.create!(bundle_id: "com.krausefx.app", 187 | //certificate: cert, 188 | //name: "Profile Name") 189 | // 190 | // Store the new profile on the filesystem 191 | //File.write("NewProfile.mobileprovision", profile.download) 192 | 193 | // Repair all broken provisioning profiles 194 | 195 | // Select all 'Invalid' or 'Expired' provisioning profiles 196 | //broken_profiles = Shuttle.DevPortal.provisioning_profile.all.find_all do |profile| 197 | // the below could be replaced with `!profile.valid? || !profile.certificate_valid?`, which takes longer but also verifies the code signing identity 198 | //(profile.status == "Invalid" or profile.status == "Expired") 199 | //end 200 | // 201 | // Iterate over all broken profiles and repair them 202 | //broken_profiles.each do |profile| 203 | //profile.repair! yes, that's all you need to repair a profile 204 | //end 205 | // 206 | // or to do the same thing, just more Ruby like 207 | //Shuttle.DevPortal.provisioning_profile.all.find_all { |p| !p.valid? || !p.certificate_valid? }.map(&:repair!) 208 | 209 | // MARK: - Devices 210 | 211 | // Get all enabled devices 212 | //all_devices = Shuttle.DevPortal.device.all 213 | // 214 | // Disable first device 215 | //all_devices.first.disable! 216 | // 217 | // Find disabled device and enable it 218 | //Shuttle.DevPortal.device.find_by_udid("44ee59893cb...", include_disabled: true).enable! 219 | // 220 | // Get list of all devices, including disabled ones, and filter the result to only include disabled devices use enabled? or disabled? methods 221 | //disabled_devices = Shuttle.DevPortal.device.all(include_disabled: true).select do |device| 222 | //!device.enabled? 223 | //end 224 | // 225 | // or to do the same thing, just more Ruby like with disabled? method 226 | //disabled_devices = Shuttle.DevPortal.device.all(include_disabled: true).select(&:disabled?) 227 | // 228 | // Register a new device 229 | //Shuttle.DevPortal.device.create!(name: "Private iPhone 6", udid: "5814abb3...") 230 | 231 | // MARK: - Enterprise 232 | 233 | // Use the InHouse class to get all enterprise certificates 234 | //cert = Shuttle.DevPortal.certificate.in_house.all.first 235 | // 236 | // Create a new InHouse Enterprise distribution profile 237 | //profile = Shuttle.DevPortal.provisioning_profile.in_house.create!(bundle_id: "com.krausefx.*", 238 | //certificate: cert) 239 | // 240 | // List all In-House Provisioning Profiles 241 | //profiles = Shuttle.DevPortal.provisioning_profile.in_house.all 242 | 243 | // MARK: - Multiple Shuttles 244 | 245 | //spaceship1 = Spaceship.Launcher.new("felix@krausefx.com", "password") 246 | //spaceship2 = Spaceship.Launcher.new("stefan@spaceship.airforce", "password") 247 | 248 | } catch let error { 249 | print("Error thrown: \(error)") 250 | } 251 | -------------------------------------------------------------------------------- /Sources/TestFlight/Models/AppTestInfo.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/TestFlight/Models/AppTestInfo.swift -------------------------------------------------------------------------------- /Sources/TestFlight/Models/BetaReviewInfo.swift: -------------------------------------------------------------------------------- 1 | 2 | struct BetaReviewInfo { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /Sources/TestFlight/Models/Build.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct Build: Codable { 3 | enum BuildState: String, Codable { 4 | case processing = "testflight.build.state.processing" 5 | case active = "testflight.build.state.testing.active" 6 | case readyToSubmit = "testflight.build.state.submit.ready" 7 | case readyToTest = "testflight.build.state.testing.ready" 8 | case exportComplianceMissing = "testflight.build.state.export.compliance.missing" 9 | } 10 | 11 | let id: String 12 | let appId: String // appAdamId 13 | let providerId: String 14 | let bundleId: String 15 | let trainVersion: String 16 | let buildVersion: String 17 | let betaReviewInfo: String 18 | let exportCompliance: String 19 | let internalState: String 20 | let externalState: BuildState 21 | let testInfo: String 22 | let installCount: String 23 | let inviteCount: String 24 | let crashCount: String 25 | let didNotify: String 26 | let uploadDate: String 27 | 28 | var isProcessed: Bool { 29 | switch externalState { 30 | case .active, .readyToSubmit, .exportComplianceMissing: 31 | return true 32 | default: 33 | return false 34 | } 35 | } 36 | 37 | static func find(appId: String? = nil, buildId: String? = nil) -> [Build] { 38 | return [Build]() 39 | } 40 | 41 | static func all(appId: String? = nil, platform: String? = nil) -> [Build] { 42 | return [Build]() 43 | } 44 | 45 | static func buildsForTrain(appId: String? = nil, platform: String? = nil, version: String? = nil, retryCount: Int = 0) -> [Build] { 46 | return [Build]() 47 | } 48 | static func allProcessingBuilds(appId: String? = nil, platform: String? = nil) -> [Build] { 49 | return [Build]() 50 | } 51 | 52 | // static func latest(appId: String? = nil, platform: String? = nil) -> Build { 53 | // return Build() 54 | // } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/TestFlight/Models/BuildTrains.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/TestFlight/Models/BuildTrains.swift -------------------------------------------------------------------------------- /Sources/TestFlight/Models/ExportCompliance.swift: -------------------------------------------------------------------------------- 1 | 2 | struct ExportCompliance { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /Sources/TestFlight/Models/Group.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/TestFlight/Models/Group.swift -------------------------------------------------------------------------------- /Sources/TestFlight/Models/TestInfo.swift: -------------------------------------------------------------------------------- 1 | 2 | struct TestInfo { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /Sources/TestFlight/Models/Tester.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/TestFlight/Models/Tester.swift -------------------------------------------------------------------------------- /Sources/TestFlight/TestFlight+API.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | import Foundation 3 | import MoyaSugar 4 | 5 | enum TestFlightAPI { 6 | 7 | } 8 | 9 | extension TestFlightAPI: SugarTargetType { 10 | var route: Route { 11 | fatalError("Not implemented") 12 | } 13 | 14 | var baseURL: URL { 15 | fatalError("Not implemented") 16 | } 17 | 18 | var parameters: Parameters? { 19 | return nil 20 | } 21 | 22 | var headers: [String : String]? { 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/TestFlight/TestFlight+Client.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | import Moya 3 | 4 | final class TestFlightClient: ShuttleCore.Client { 5 | 6 | let clientProvider = MoyaProvider() 7 | 8 | // var teams = [Team]() 9 | 10 | // required init(cookie: String? = nil, teamId: String? = nil) { 11 | // super.init() 12 | // } 13 | 14 | // static func login(email: String, password: String) -> TestFlightClient { 15 | // return TestFlightClient() 16 | // } 17 | 18 | // func sendLoginRequest(email: String, password: String) { 19 | // 20 | // } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/TestFlight/TestFlight.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | 3 | public struct TestFlight { 4 | public static var text = "Hello, World!" 5 | } 6 | 7 | -------------------------------------------------------------------------------- /Sources/TestSupport/NetworkResponseStubs.swift: -------------------------------------------------------------------------------- 1 | public enum Mock { 2 | public static let signIn200 = "{}" 3 | 4 | public static let authService200 = """ 5 | { 6 | "authServiceUrl": "https://idmsa.apple.com/appleauth", 7 | "authServiceKey": "e0b80c3bf78523bfe80974d320935bfa30add02e1bff88ec2166c6bd5a706c42" 8 | } 9 | """ 10 | 11 | public static let olympusSession200 = """ 12 | { 13 | "user": { 14 | "fullName": "Kaden Wilkinson", 15 | "emailAddress": "example@example.com", 16 | "prsId": "11219833707" 17 | }, 18 | "provider": { 19 | "providerId": 1234567, 20 | "name": "Company, Inc.", 21 | "contentTypes": [ 22 | "SOFTWARE" 23 | ] 24 | }, 25 | "availableProviders": [ 26 | { 27 | "providerId": 7654321, 28 | "name": "My Company, Inc.", 29 | "contentTypes": [ 30 | "SOFTWARE" 31 | ] 32 | } 33 | ], 34 | "backingType": "ITC", 35 | "roles": [ 36 | "ADMIN" 37 | ], 38 | "unverifiedRoles": [], 39 | "featureFlags": [], 40 | "agreeToTerms": true, 41 | "modules": [ 42 | { 43 | "key": "Apps", 44 | "name": "ITC.HomePage.Apps.IconText", 45 | "localizedName": "My Apps", 46 | "url": "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app", 47 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/MyApps.png", 48 | "down": false, 49 | "hasNotifications": false 50 | }, 51 | { 52 | "key": "AppAnalytics", 53 | "name": "ITC.HomePage.AppAnalytics.IconText", 54 | "localizedName": "App Analytics", 55 | "url": "https://analytics.itunes.apple.com/", 56 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/AppAnalytics.png", 57 | "down": false, 58 | "hasNotifications": false 59 | }, 60 | { 61 | "key": "SalesTrends", 62 | "name": "ITC.HomePage.SalesTrends.IconText", 63 | "localizedName": "Sales and Trends", 64 | "url": "https://reportingitc2.apple.com/?", 65 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/SalesandTrends.png", 66 | "down": false, 67 | "hasNotifications": false 68 | }, 69 | { 70 | "key": "FinancialReports", 71 | "name": "ITC.HomePage.FinancialReports.IconText", 72 | "localizedName": "Payments and Financial Reports", 73 | "url": "https://itunesconnect.apple.com/itc/payments_and_financial_reports", 74 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/financialReports.png", 75 | "down": false, 76 | "hasNotifications": false 77 | }, 78 | { 79 | "key": "ManageUsers", 80 | "name": "ITC.HomePage.ManageUsers.IconText", 81 | "localizedName": "Users and Roles", 82 | "url": "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/users_roles", 83 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/UsersandRoles.png", 84 | "down": false, 85 | "hasNotifications": false 86 | }, 87 | { 88 | "key": "ContractsTaxBanking", 89 | "name": "ITC.HomePage.ContractsTaxBanking.IconText", 90 | "localizedName": "Agreements, Tax, and Banking", 91 | "url": "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", 92 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/AgreementsandBanking.png", 93 | "down": false, 94 | "hasNotifications": false 95 | }, 96 | { 97 | "key": "Resources", 98 | "name": "ITC.HomePage.Resources.IconText", 99 | "localizedName": "Resources and Help", 100 | "url": "https://itunespartner.apple.com/", 101 | "iconUrl": "https://itunesconnect.apple.com/itc/img/ico_homepage/resourcesAndhelp.png", 102 | "down": false, 103 | "hasNotifications": false 104 | } 105 | ], 106 | "helpLinks": [ 107 | { 108 | "key": "All", 109 | "url": "https://itunespartner.apple.com", 110 | "localizedText": "All Resources and Help" 111 | }, 112 | { 113 | "key": "News", 114 | "url": "https://itunespartner.apple.com/news/", 115 | "localizedText": "News" 116 | }, 117 | { 118 | "key": "Guides", 119 | "url": "https://itunespartner.apple.com/guides/", 120 | "localizedText": "Guides" 121 | }, 122 | { 123 | "key": "Videos", 124 | "url": "https://itunespartner.apple.com/videos/", 125 | "localizedText": "Videos" 126 | }, 127 | { 128 | "key": "FAQ", 129 | "url": "https://itunespartner.apple.com/faq/", 130 | "localizedText": "FAQ" 131 | }, 132 | { 133 | "key": "ContactUs", 134 | "url": "https://www.apple.com/itunes/go/itunesconnect/contactus", 135 | "localizedText": "Contact Us" 136 | } 137 | ] 138 | } 139 | """ 140 | 141 | public static let twoFactorCodeInfo200 = """ 142 | { 143 | "trustedPhoneNumbers": [ 144 | { 145 | "numberWithDialCode": "•• (•••) •••-••12", 146 | "pushMode": "sms", 147 | "obfuscatedNumber": "(•••) •••-••12", 148 | "id": 1 149 | } 150 | ], 151 | "securityCode": { 152 | "length": 6, 153 | "tooManyCodesSent": false, 154 | "tooManyCodesValidated": false, 155 | "securityCodeLocked": false 156 | }, 157 | "authenticationType": "hsa2", 158 | "recoveryUrl": "https://iforgot.apple.com/phone/add?prs_account_nm=example@gmail.com&autoSubmitAccount=true&appId=142", 159 | "cantUsePhoneNumberUrl": "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=example@gmail.com&autoSubmitAccount=true&appId=142", 160 | "recoveryWebUrl": "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=example@gmail.com&autoSubmitAccount=true&appId=142", 161 | "repairPhoneNumberUrl": "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", 162 | "repairPhoneNumberWebUrl": "https://appleid.apple.com/widget/account/repair?#!repair", 163 | "aboutTwoFactorAuthenticationUrl": "https://support.apple.com/kb/HT204921", 164 | "autoVerified": false, 165 | "showAutoVerificationUI": false, 166 | "managedAccount": false, 167 | "trustedPhoneNumber": { 168 | "numberWithDialCode": "•• (•••) •••-••12", 169 | "pushMode": "sms", 170 | "obfuscatedNumber": "(•••) •••-••612", 171 | "id": 1 172 | }, 173 | "supportsRecovery": true, 174 | "hsa2Account": true 175 | } 176 | """ 177 | } 178 | -------------------------------------------------------------------------------- /Sources/Tunes/ Models/App.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/Tunes/ Models/App.swift -------------------------------------------------------------------------------- /Sources/Tunes/ Models/IAP.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/Tunes/ Models/IAP.swift -------------------------------------------------------------------------------- /Sources/Tunes/ Models/Member.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutobahnSwift/Shuttle/5c35f1993572d29b11afffe215251c50cc86a180/Sources/Tunes/ Models/Member.swift -------------------------------------------------------------------------------- /Sources/Tunes/Tunes+Client.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | import Moya 3 | 4 | final class TunesClient: ShuttleCore.Client { 5 | 6 | // let clientProvider = MoyaProvider() 7 | // var teams = [Team]() 8 | // 9 | // required init(cookie: String? = nil, teamId: String? = nil) { 10 | // 11 | // } 12 | // 13 | // static func login(email: String, password: String) -> TunesClient { 14 | // return TunesClient() 15 | // } 16 | // 17 | // func sendLoginRequest(email: String, password: String) { 18 | // 19 | // } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Tunes/Tunes.swift: -------------------------------------------------------------------------------- 1 | import ShuttleCore 2 | 3 | public struct Tunes { 4 | public static var text = "Hello, World!" 5 | } 6 | -------------------------------------------------------------------------------- /Tests/DevPortalTests/DevPortalTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DevPortal 3 | 4 | class DevPortalTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/DevPortalTests/ProvisioningProfileTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Shuttle 2 | import XCTest 3 | 4 | class ProvisioningProfileTests: XCTestCase { 5 | func testAllProperlyRetrievesAndFiltersTheProvisioningProfiles() { 6 | 7 | } 8 | 9 | func testShouldFilterByTheCorrectTypes() { 10 | 11 | } 12 | 13 | func testAppStoreAndAdHocAreTheSame() { 14 | 15 | } 16 | 17 | func testShouldHaveAnApp() { 18 | 19 | } 20 | 21 | func testReturnsOnlyValidProfileTypes() { 22 | 23 | } 24 | 25 | func testFiltersXcodeManagedProfiles() { 26 | 27 | } 28 | 29 | func testIncludesXcodeManagedProfiles() { 30 | 31 | } 32 | 33 | func testShouldUseTheXcodeApiToGetProvisioningProfilesAndTheirAppIds() { 34 | 35 | } 36 | 37 | func testShouldUseTheDeveloperPortalApiToGetProvisioningProfilesAndTheirAppIds() { 38 | 39 | } 40 | 41 | // MARK: - findBy(bundleId:) 42 | 43 | func testFindByBundleIdReturnsEmptyArrayIfThereAreNoProfiles() { 44 | 45 | } 46 | 47 | func testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForiOS() { 48 | 49 | } 50 | 51 | func testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForTvOS() { 52 | 53 | } 54 | 55 | func testDistributionMethodStaysAppStoreEvenThoughItsAnAdHocProfileWhichContainsDevices() { 56 | 57 | } 58 | 59 | //MARK: - download 60 | 61 | func testDownloadForAnExistingProvisioningProfile() { 62 | 63 | } 64 | 65 | func testDownloadHandlesFailedDownloadRequest() { 66 | 67 | } 68 | 69 | // MARK: - isValid 70 | 71 | func testIsValidTrue() { 72 | 73 | } 74 | 75 | func testIsValidFalse() { 76 | 77 | } 78 | 79 | // MARK: - factory 80 | 81 | func testFactoryCreatesADirectProfileTypeForDistributionMethodDirect() { 82 | 83 | } 84 | 85 | // MARK: - create 86 | 87 | func testCreateANewDevelopmentProvisioningProfile() { 88 | 89 | } 90 | 91 | func testCreateANewAppstoreProvisioningProfile() { 92 | 93 | } 94 | 95 | func testCreateAProvisioningProfileWithOnlyTheRequiredParametersAndAutoFillsAllAvailableDevices() { 96 | 97 | } 98 | 99 | func testErrorThrownIfTheUserWantsToCreateAProfileForANonExistingApp() { 100 | 101 | } 102 | 103 | // MARK: - modify devices to prevent having devices on profile types where it does not make sense 104 | 105 | func testDirectOrMacProfileTypesHaveNoDevices() { 106 | 107 | } 108 | 109 | func testDevelopmentProfileTypesHaveDevices() { 110 | 111 | } 112 | 113 | func testAdHocProfileTypesHaveNoDevices() { 114 | 115 | } 116 | 117 | func testAppStoreProfileTypesHaveNoDevices() { 118 | 119 | } 120 | 121 | // MARK: - delete 122 | 123 | func testDeleteAnExistingProfile() { 124 | 125 | } 126 | 127 | // MARK: - repair 128 | 129 | func testRepairAnExistingProfileWithAddedDevices() { 130 | 131 | } 132 | 133 | func testRepairUpdatesTheCertificateIfTheCurrentOneDoesntExist() { 134 | 135 | } 136 | 137 | func testRepairUpdatesTheCertificateIfTheCurrentOneIsInvalid() { 138 | 139 | } 140 | 141 | func testRepairAnExistingProfileWithNoDevices() { 142 | 143 | } 144 | 145 | func testDifferentEnvironmentsDevelopment() { 146 | 147 | } 148 | 149 | // MARK: - update 150 | 151 | func testUpdateAnExistingiOSProfile() { 152 | 153 | } 154 | 155 | func testUpdateAnExistingTvOSProfile() { 156 | 157 | } 158 | 159 | // MARK: - isAdhoc 160 | 161 | func testIsAdhocReturnsTrueWhenTheProfileIsAdhoc() { 162 | 163 | } 164 | 165 | func testIsAdhocReturnsTrueWhenTheProfileIsAppstoreWithDevices() { 166 | 167 | } 168 | 169 | func testIsAdhocReturnsFalseWhenTheProfileIsAppstoreWithNoDevices() { 170 | 171 | } 172 | 173 | func testIsAdhocReturnsFalseWhenTheProfileIsDevelopment() { 174 | 175 | } 176 | 177 | func testIsAdhocReturnsFalseWhenTheProfileIsInhouse() { 178 | 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 0.8.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | 6 | @testable import ShuttleTests 7 | 8 | extension BuildStateTests { 9 | static var allTests: [(String, (BuildStateTests) -> () throws -> Void)] = [ 10 | ("testReady", testReady) 11 | ] 12 | } 13 | 14 | extension BuildTests { 15 | static var allTests: [(String, (BuildTests) -> () throws -> Void)] = [ 16 | ("testGetBuildByID", testGetBuildByID), 17 | ("testThrowWhenBuildNotFound", testThrowWhenBuildNotFound), 18 | ("testGetBuildsAcrossAllTrains", testGetBuildsAcrossAllTrains), 19 | ("testGetProcessingBuilds", testGetProcessingBuilds) 20 | ] 21 | } 22 | 23 | extension DecodableTests { 24 | static var allTests: [(String, (DecodableTests) -> () throws -> Void)] = [ 25 | ("testDecodeOlympusSession200", testDecodeOlympusSession200) 26 | ] 27 | } 28 | 29 | extension LauncherTests { 30 | static var allTests: [(String, (LauncherTests) -> () throws -> Void)] = [ 31 | ("testHasAClient", testHasAClient), 32 | ("testReturnsAScopedModelClass", testReturnsAScopedModelClass), 33 | ("testPassesTheClientToTheModels", testPassesTheClientToTheModels) 34 | ] 35 | } 36 | 37 | extension PersistentCookieTests { 38 | static var allTests: [(String, (PersistentCookieTests) -> () throws -> Void)] = [ 39 | ("testUsesEnvWhenSet", testUsesEnvWhenSet), 40 | ("testUsesHomeDirByDefault", testUsesHomeDirByDefault), 41 | ("testUsesTmpDirIfHomeNotAvailable", testUsesTmpDirIfHomeNotAvailable), 42 | ("testFallsBackToTmpDirAsLastResort", testFallsBackToTmpDirAsLastResort) 43 | ] 44 | } 45 | 46 | extension PortalTests { 47 | static var allTests: [(String, (PortalTests) -> () throws -> Void)] = [ 48 | ("testExample", testExample) 49 | ] 50 | } 51 | 52 | extension ProvisioningProfileTests { 53 | static var allTests: [(String, (ProvisioningProfileTests) -> () throws -> Void)] = [ 54 | ("testAllProperlyRetrievesAndFiltersTheProvisioningProfiles", testAllProperlyRetrievesAndFiltersTheProvisioningProfiles), 55 | ("testShouldFilterByTheCorrectTypes", testShouldFilterByTheCorrectTypes), 56 | ("testAppStoreAndAdHocAreTheSame", testAppStoreAndAdHocAreTheSame), 57 | ("testShouldHaveAnApp", testShouldHaveAnApp), 58 | ("testReturnsOnlyValidProfileTypes", testReturnsOnlyValidProfileTypes), 59 | ("testFiltersXcodeManagedProfiles", testFiltersXcodeManagedProfiles), 60 | ("testIncludesXcodeManagedProfiles", testIncludesXcodeManagedProfiles), 61 | ("testShouldUseTheXcodeApiToGetProvisioningProfilesAndTheirAppIds", testShouldUseTheXcodeApiToGetProvisioningProfilesAndTheirAppIds), 62 | ("testShouldUseTheDeveloperPortalApiToGetProvisioningProfilesAndTheirAppIds", testShouldUseTheDeveloperPortalApiToGetProvisioningProfilesAndTheirAppIds), 63 | ("testFindByBundleIdReturnsEmptyArrayIfThereAreNoProfiles", testFindByBundleIdReturnsEmptyArrayIfThereAreNoProfiles), 64 | ("testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForiOS", testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForiOS), 65 | ("testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForTvOS", testFindByBundleIdReturnsTheProfileInAnArrayIfMatchingForTvOS), 66 | ("testDistributionMethodStaysAppStoreEvenThoughItsAnAdHocProfileWhichContainsDevices", testDistributionMethodStaysAppStoreEvenThoughItsAnAdHocProfileWhichContainsDevices), 67 | ("testDownloadForAnExistingProvisioningProfile", testDownloadForAnExistingProvisioningProfile), 68 | ("testDownloadHandlesFailedDownloadRequest", testDownloadHandlesFailedDownloadRequest), 69 | ("testIsValidTrue", testIsValidTrue), 70 | ("testIsValidFalse", testIsValidFalse), 71 | ("testFactoryCreatesADirectProfileTypeForDistributionMethodDirect", testFactoryCreatesADirectProfileTypeForDistributionMethodDirect), 72 | ("testCreateANewDevelopmentProvisioningProfile", testCreateANewDevelopmentProvisioningProfile), 73 | ("testCreateANewAppstoreProvisioningProfile", testCreateANewAppstoreProvisioningProfile), 74 | ("testCreateAProvisioningProfileWithOnlyTheRequiredParametersAndAutoFillsAllAvailableDevices", testCreateAProvisioningProfileWithOnlyTheRequiredParametersAndAutoFillsAllAvailableDevices), 75 | ("testErrorThrownIfTheUserWantsToCreateAProfileForANonExistingApp", testErrorThrownIfTheUserWantsToCreateAProfileForANonExistingApp), 76 | ("testDirectOrMacProfileTypesHaveNoDevices", testDirectOrMacProfileTypesHaveNoDevices), 77 | ("testDevelopmentProfileTypesHaveDevices", testDevelopmentProfileTypesHaveDevices), 78 | ("testAdHocProfileTypesHaveNoDevices", testAdHocProfileTypesHaveNoDevices), 79 | ("testAppStoreProfileTypesHaveNoDevices", testAppStoreProfileTypesHaveNoDevices), 80 | ("testDeleteAnExistingProfile", testDeleteAnExistingProfile), 81 | ("testRepairAnExistingProfileWithAddedDevices", testRepairAnExistingProfileWithAddedDevices), 82 | ("testRepairUpdatesTheCertificateIfTheCurrentOneDoesntExist", testRepairUpdatesTheCertificateIfTheCurrentOneDoesntExist), 83 | ("testRepairUpdatesTheCertificateIfTheCurrentOneIsInvalid", testRepairUpdatesTheCertificateIfTheCurrentOneIsInvalid), 84 | ("testRepairAnExistingProfileWithNoDevices", testRepairAnExistingProfileWithNoDevices), 85 | ("testDifferentEnvironmentsDevelopment", testDifferentEnvironmentsDevelopment), 86 | ("testUpdateAnExistingiOSProfile", testUpdateAnExistingiOSProfile), 87 | ("testUpdateAnExistingTvOSProfile", testUpdateAnExistingTvOSProfile), 88 | ("testIsAdhocReturnsTrueWhenTheProfileIsAdhoc", testIsAdhocReturnsTrueWhenTheProfileIsAdhoc), 89 | ("testIsAdhocReturnsTrueWhenTheProfileIsAppstoreWithDevices", testIsAdhocReturnsTrueWhenTheProfileIsAppstoreWithDevices), 90 | ("testIsAdhocReturnsFalseWhenTheProfileIsAppstoreWithNoDevices", testIsAdhocReturnsFalseWhenTheProfileIsAppstoreWithNoDevices), 91 | ("testIsAdhocReturnsFalseWhenTheProfileIsDevelopment", testIsAdhocReturnsFalseWhenTheProfileIsDevelopment), 92 | ("testIsAdhocReturnsFalseWhenTheProfileIsInhouse", testIsAdhocReturnsFalseWhenTheProfileIsInhouse) 93 | ] 94 | } 95 | 96 | extension RetryTests { 97 | static var allTests: [(String, (RetryTests) -> () throws -> Void)] = [ 98 | ("testReRaisesWhenRetryLimitReachedThrowingTimeoutError", testReRaisesWhenRetryLimitReachedThrowingTimeoutError), 99 | ("testReRaisesWhenRetryLimitReachedThrowingConnectionFailed", testReRaisesWhenRetryLimitReachedThrowingConnectionFailed), 100 | ("testRetriesWhenTimeoutErrorThrown", testRetriesWhenTimeoutErrorThrown), 101 | ("testRetriesWhenConnectionFailedErrorThrown", testRetriesWhenConnectionFailedErrorThrown), 102 | ("testRaisesAppleTimeoutErrorWhenResponseContains302Found", testRaisesAppleTimeoutErrorWhenResponseContains302Found), 103 | ("testSuccessfullyRetriesRequestAfterLoggingInAgainWhenUnauthorizedAccessErrorThrown", testSuccessfullyRetriesRequestAfterLoggingInAgainWhenUnauthorizedAccessErrorThrown), 104 | ("testFailsToRetryRequestIfLoginFailsInRetryBlockWhenUnauthorizedAccessErrorThrown", testFailsToRetryRequestIfLoginFailsInRetryBlockWhenUnauthorizedAccessErrorThrown), 105 | ("testRetryWhenUserAndPasswordNotFetchedFromCredentialManagerIsAbleToRetryAndLoginSuccessfully", testRetryWhenUserAndPasswordNotFetchedFromCredentialManagerIsAbleToRetryAndLoginSuccessfully) 106 | ] 107 | } 108 | 109 | extension ShuttleTests { 110 | static var allTests: [(String, (ShuttleTests) -> () throws -> Void)] = [ 111 | ("testSelectTeam", testSelectTeam), 112 | ("testShouldInitializeWithAClient", testShouldInitializeWithAClient), 113 | ("testDevice", testDevice), 114 | ("testCertificate", testCertificate), 115 | ("testProvisioningProfile", testProvisioningProfile), 116 | ("testApp", testApp), 117 | ("testAppGroup", testAppGroup) 118 | ] 119 | } 120 | 121 | extension TestFlightTests { 122 | static var allTests: [(String, (TestFlightTests) -> () throws -> Void)] = [ 123 | ("testExample", testExample) 124 | ] 125 | } 126 | 127 | extension TunesTests { 128 | static var allTests: [(String, (TunesTests) -> () throws -> Void)] = [ 129 | ("testExample", testExample) 130 | ] 131 | } 132 | 133 | extension TwoStepAuthTests { 134 | static var allTests: [(String, (TwoStepAuthTests) -> () throws -> Void)] = [ 135 | ("testExample", testExample) 136 | ] 137 | } 138 | 139 | XCTMain([ 140 | testCase(BuildStateTests.allTests), 141 | testCase(BuildTests.allTests), 142 | testCase(DecodableTests.allTests), 143 | testCase(LauncherTests.allTests), 144 | testCase(PersistentCookieTests.allTests), 145 | testCase(PortalTests.allTests), 146 | testCase(ProvisioningProfileTests.allTests), 147 | testCase(RetryTests.allTests), 148 | testCase(ShuttleTests.allTests), 149 | testCase(TestFlightTests.allTests), 150 | testCase(TunesTests.allTests), 151 | testCase(TwoStepAuthTests.allTests), 152 | ]) 153 | -------------------------------------------------------------------------------- /Tests/ShuttleCoreTests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ShuttleCore 2 | import XCTest 3 | 4 | class TestClient: ShuttleCore.Client { 5 | 6 | } 7 | 8 | class RetryTests: XCTestCase { 9 | override func setUp() { 10 | super.setUp() 11 | } 12 | 13 | override func tearDown() { 14 | super.tearDown() 15 | } 16 | 17 | func testReRaisesWhenRetryLimitReachedThrowingTimeoutError() { 18 | 19 | } 20 | 21 | func testReRaisesWhenRetryLimitReachedThrowingConnectionFailed() { 22 | 23 | } 24 | 25 | func testRetriesWhenTimeoutErrorThrown() { 26 | 27 | } 28 | 29 | func testRetriesWhenConnectionFailedErrorThrown() { 30 | 31 | } 32 | 33 | func testRaisesAppleTimeoutErrorWhenResponseContains302Found() { 34 | 35 | } 36 | 37 | func testSuccessfullyRetriesRequestAfterLoggingInAgainWhenUnauthorizedAccessErrorThrown() { 38 | 39 | } 40 | 41 | func testFailsToRetryRequestIfLoginFailsInRetryBlockWhenUnauthorizedAccessErrorThrown() { 42 | 43 | } 44 | 45 | func testRetryWhenUserAndPasswordNotFetchedFromCredentialManagerIsAbleToRetryAndLoginSuccessfully() { 46 | 47 | } 48 | } 49 | 50 | class PersistentCookieTests: XCTestCase { 51 | override func setUp() { 52 | super.setUp() 53 | } 54 | 55 | override func tearDown() { 56 | super.tearDown() 57 | } 58 | 59 | func testUsesEnvWhenSet() { 60 | 61 | } 62 | 63 | func testUsesHomeDirByDefault() { 64 | 65 | } 66 | 67 | func testUsesTmpDirIfHomeNotAvailable() { 68 | 69 | } 70 | 71 | func testFallsBackToTmpDirAsLastResort() { 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/ShuttleCoreTests/DecodableTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ShuttleCore 2 | import TestSupport 3 | import XCTest 4 | 5 | class DecodableTests: XCTestCase { 6 | let decoder = JSONDecoder() 7 | 8 | func testDecodeOlympusSession200() throws { 9 | let data = Mock.olympusSession200.data(using: .utf8)! 10 | _ = try decoder.decode(OlympusSessionResponse.self, from: data) 11 | } 12 | 13 | func testDecodeTwoFactorCodeInfo200() throws { 14 | let data = Mock.twoFactorCodeInfo200.data(using: .utf8)! 15 | _ = try decoder.decode(TwoFactorAuthResponse.self, from: data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ShuttleCoreTests/TwoStepAuthTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ShuttleCore 2 | import XCTest 3 | 4 | class TwoStepAuthTests: XCTestCase { 5 | override func setUp() { 6 | super.setUp() 7 | } 8 | 9 | override func tearDown() { 10 | super.tearDown() 11 | } 12 | 13 | func testExample() { 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/ShuttleTests/ShuttleTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Shuttle 2 | import XCTest 3 | 4 | class ShuttleTests: XCTestCase { 5 | 6 | func testExample() { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/TestFlightTests/BuildTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Shuttle 2 | import XCTest 3 | 4 | 5 | class BuildTests: XCTestCase { 6 | func testGetBuildByID() { 7 | 8 | } 9 | 10 | func testThrowWhenBuildNotFound() { 11 | 12 | } 13 | 14 | func testGetBuildsAcrossAllTrains() { 15 | 16 | } 17 | 18 | func testGetProcessingBuilds() { 19 | 20 | } 21 | } 22 | 23 | class BuildStateTests: XCTestCase { 24 | func testReady() { 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/TestFlightTests/TestFlightTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TestFlight 3 | 4 | class TestFlightTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(TestFlight.text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/TunesTests/TunesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Tunes 3 | 4 | class TunesTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Tunes.text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | config.vm.box = "ubuntu/xenial64" 7 | config.vm.network "private_network", ip: "192.168.33.10" 8 | config.vm.hostname = "xenial64" 9 | config.vm.synced_folder ".", "/home/ubuntu/swift-package", :mount_options => ["dmode=775", "fmode=776"] 10 | 11 | # Optional NFS. Make sure to remove other synced_folder line too 12 | #config.vm.synced_folder ".", "/var/www", :nfs => { :mount_options => ["dmode=777","fmode=666"] } 13 | 14 | config.vm.provider "virtualbox" do |vm| 15 | vm.memory = 4096 16 | vm.cpus = 4 17 | end 18 | end 19 | --------------------------------------------------------------------------------