├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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 |
--------------------------------------------------------------------------------