Rearrange targets for unit testing (#900)

- Move and rename entities
- Split protocols and default extensions
- Move in-app entities to CommonIAP target
This commit is contained in:
Davide 2024-11-20 18:05:47 +01:00 committed by GitHub
parent 2ccd3052ac
commit b45f9c23fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 410 additions and 208 deletions

View File

@ -105,6 +105,34 @@
ReferencedContainer = "container:"> ReferencedContainer = "container:">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PassepartoutImplementations"
BuildableName = "PassepartoutImplementations"
BlueprintName = "PassepartoutImplementations"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CommonIAP"
BuildableName = "CommonIAP"
BlueprintName = "CommonIAP"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction

View File

@ -41,8 +41,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : { "state" : {
"revision" : "3a4c78af67dfe181acc657a5539ee3d62d1c9361", "revision" : "d11036e59b65601b617120471dc9a469567388f5"
"version" : "0.11.0"
} }
}, },
{ {

View File

@ -15,11 +15,15 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages. // Products define the executables and libraries a package produces, making them visible to other packages.
.library( .library(
name: "AppUIMain", name: "AppUIMain",
targets: ["AppUIMain"] targets: ["AppUIMainWrapper"]
), ),
.library( .library(
name: "AppUITV", name: "AppUITV",
targets: ["AppUITV"] targets: ["AppUITVWrapper"]
),
.library(
name: "CommonIAP",
targets: ["CommonIAP"]
), ),
.library( .library(
name: "CommonLibrary", name: "CommonLibrary",
@ -95,10 +99,22 @@ let package = Package(
.process("Resources") .process("Resources")
] ]
), ),
.target(
name: "AppUIMainWrapper",
dependencies: [
.target(name: "AppUIMain", condition: .when(platforms: [.iOS, .macOS]))
]
),
.target( .target(
name: "AppUITV", name: "AppUITV",
dependencies: ["UILibrary"] dependencies: ["UILibrary"]
), ),
.target(
name: "AppUITVWrapper",
dependencies: [
.target(name: "AppUITV", condition: .when(platforms: [.tvOS]))
]
),
.target( .target(
name: "CommonAPI", name: "CommonAPI",
dependencies: ["CommonLibrary"], dependencies: ["CommonLibrary"],
@ -106,9 +122,14 @@ let package = Package(
.copy("API") .copy("API")
] ]
), ),
.target(
name: "CommonIAP",
dependencies: ["CommonUtils"]
),
.target( .target(
name: "CommonLibrary", name: "CommonLibrary",
dependencies: [ dependencies: [
"CommonIAP",
"CommonUtils", "CommonUtils",
.product(name: "PassepartoutKit", package: "passepartoutkit-source") .product(name: "PassepartoutKit", package: "passepartoutkit-source")
], ],

View File

@ -27,6 +27,7 @@
import AppKit import AppKit
import CommonLibrary import CommonLibrary
import CommonUtils
import PassepartoutKit import PassepartoutKit
import ServiceManagement import ServiceManagement

View File

@ -1,5 +1,5 @@
// //
// ProviderEntityViewProviding+Extensions.swift // ProviderEntityViewProviding+VPN.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 10/29/24. // Created by Davide De Rosa on 10/29/24.

View File

@ -25,9 +25,10 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension DNSModule.Builder: ModuleViewProviding { extension DNSModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
DNSView(editor: editor, module: self) DNSView(editor: editor, module: self)
} }
} }

View File

@ -25,9 +25,10 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension HTTPProxyModule.Builder: ModuleViewProviding { extension HTTPProxyModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
HTTPProxyView(editor: editor, module: self) HTTPProxyView(editor: editor, module: self)
} }
} }

View File

@ -25,9 +25,10 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension IPModule.Builder: ModuleViewProviding { extension IPModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
IPView(editor: editor, module: self) IPView(editor: editor, module: self)
} }
} }

View File

@ -25,9 +25,10 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension OnDemandModule.Builder: ModuleViewProviding { extension OnDemandModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
OnDemandView(editor: editor, module: self) OnDemandView(editor: editor, module: self)
} }
} }

View File

@ -26,15 +26,16 @@
import CommonUtils import CommonUtils
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension OpenVPNModule.Builder: ModuleViewProviding { extension OpenVPNModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation) OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation)
} }
} }
extension OpenVPNModule: ProviderEntityViewProviding { extension OpenVPNModule: ProviderEntityViewProviding {
func providerEntityView( public func providerEntityView(
with provider: SerializedProvider, with provider: SerializedProvider,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void

View File

@ -26,15 +26,16 @@
import CommonUtils import CommonUtils
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UILibrary
extension WireGuardModule.Builder: ModuleViewProviding { extension WireGuardModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation) WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation)
} }
} }
extension WireGuardModule: ProviderEntityViewProviding { extension WireGuardModule: ProviderEntityViewProviding {
func providerEntityView( public func providerEntityView(
with provider: SerializedProvider, with provider: SerializedProvider,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void

View File

@ -41,15 +41,6 @@ public enum AppFeature: String, CaseIterable {
case routing case routing
case sharing case sharing
public static let fullV2Features: [AppFeature] = [
.dns,
.httpProxy,
.onDemand,
.providers,
.routing,
.sharing
]
} }
extension AppFeature: Identifiable { extension AppFeature: Identifiable {

View File

@ -0,0 +1,30 @@
//
// AppFeatureProviding.swift
// Passepartout
//
// Created by Davide De Rosa on 10/11/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
protocol AppFeatureProviding {
var features: [AppFeature] { get }
}

View File

@ -0,0 +1,30 @@
//
// AppFeatureRequiring.swift
// Passepartout
//
// Created by Davide De Rosa on 11/17/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
public protocol AppFeatureRequiring {
var features: Set<AppFeature> { get }
}

View File

@ -23,9 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CommonUtils
import Foundation import Foundation
import PassepartoutKit
public struct AppProduct: RawRepresentable, Hashable, Sendable { public struct AppProduct: RawRepresentable, Hashable, Sendable {
public let rawValue: String public let rawValue: String

View File

@ -35,8 +35,10 @@ public enum AppUserLevel: Int, Sendable {
case fullV2 = 2 case fullV2 = 2
case subscriber = 3 case subscriber = 3
}
var isFullVersion: Bool { extension AppUserLevel {
public var isFullVersion: Bool {
switch self { switch self {
case .fullV2, .subscriber: case .fullV2, .subscriber:
return true return true
@ -46,7 +48,7 @@ public enum AppUserLevel: Int, Sendable {
} }
} }
var isRestricted: Bool { public var isRestricted: Bool {
switch self { switch self {
case .undefined, .beta: case .undefined, .beta:
return true return true

View File

@ -0,0 +1,37 @@
//
// AppFeature+Full.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
extension AppFeature {
public static let fullV2Features: [AppFeature] = [
.dns,
.httpProxy,
.onDemand,
.providers,
.routing,
.sharing
]
}

View File

@ -1,8 +1,8 @@
// //
// Bundle+Extensions.swift // AppFeatureProviding+Levels.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 8/27/24. // Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -25,16 +25,20 @@
import Foundation import Foundation
extension Bundle { extension AppUserLevel: AppFeatureProviding {
public func unsafeDecode<T: Decodable>(_ type: T.Type, filename: String) -> T { public var features: [AppFeature] {
guard let jsonURL = url(forResource: filename, withExtension: "json") else { switch self {
fatalError("Unable to find \(filename).json in bundle") case .beta:
} return [.interactiveLogin, .sharing]
do {
let data = try Data(contentsOf: jsonURL) case .fullV2:
return try JSONDecoder().decode(type, from: data) return AppFeature.fullV2Features
} catch {
fatalError("Unable to decode \(filename).json: \(error)") case .subscriber:
return AppFeature.allCases
default:
return []
} }
} }
} }

View File

@ -1,8 +1,8 @@
// //
// AppFeatureProviding.swift // AppFeatureProviding+Products.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 10/11/24. // Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -25,30 +25,8 @@
import Foundation import Foundation
protocol AppFeatureProviding {
var features: [AppFeature] { get }
}
extension AppUserLevel: AppFeatureProviding {
var features: [AppFeature] {
switch self {
case .beta:
return [.interactiveLogin, .sharing]
case .fullV2:
return AppFeature.fullV2Features
case .subscriber:
return AppFeature.allCases
default:
return []
}
}
}
extension AppProduct: AppFeatureProviding { extension AppProduct: AppFeatureProviding {
var features: [AppFeature] { public var features: [AppFeature] {
switch self { switch self {
// MARK: Current // MARK: Current

View File

@ -1,5 +1,5 @@
// //
// MockAppProductHelper.swift // FakeAppProductHelper.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 12/19/23. // Created by Davide De Rosa on 12/19/23.
@ -27,12 +27,12 @@ import Combine
import CommonUtils import CommonUtils
import Foundation import Foundation
public actor MockAppProductHelper: AppProductHelper { public actor FakeAppProductHelper: AppProductHelper {
private let build: Int private let build: Int
public private(set) var products: [AppProduct: InAppProduct] public private(set) var products: [AppProduct: InAppProduct]
public nonisolated let receiptReader: MockAppReceiptReader public nonisolated let receiptReader: FakeAppReceiptReader
private nonisolated let didUpdateSubject: PassthroughSubject<Void, Never> private nonisolated let didUpdateSubject: PassthroughSubject<Void, Never>
@ -40,7 +40,7 @@ public actor MockAppProductHelper: AppProductHelper {
public init(build: Int = .max) { public init(build: Int = .max) {
self.build = build self.build = build
products = [:] products = [:]
receiptReader = MockAppReceiptReader() receiptReader = FakeAppReceiptReader()
didUpdateSubject = PassthroughSubject() didUpdateSubject = PassthroughSubject()
} }

View File

@ -1,5 +1,5 @@
// //
// MockAppReceiptReader.swift // FakeAppReceiptReader.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 12/19/23. // Created by Davide De Rosa on 12/19/23.
@ -26,7 +26,7 @@
import CommonUtils import CommonUtils
import Foundation import Foundation
public actor MockAppReceiptReader: AppReceiptReader { public actor FakeAppReceiptReader: AppReceiptReader {
private var localReceipt: InAppReceipt? private var localReceipt: InAppReceipt?
public init(receipt localReceipt: InAppReceipt? = nil) { public init(receipt localReceipt: InAppReceipt? = nil) {

View File

@ -128,13 +128,6 @@ extension IAPManager {
features.allSatisfy(eligibleFeatures.contains) features.allSatisfy(eligibleFeatures.contains)
} }
public func isEligible(forProvider providerId: ProviderID) -> Bool {
if providerId == .oeck {
return true
}
return isEligible(for: .providers)
}
public func isEligibleForFeedback() -> Bool { public func isEligibleForFeedback() -> Bool {
#if os(tvOS) #if os(tvOS)
false false

View File

@ -68,7 +68,6 @@ extension ProfileAttributes: CustomDebugStringConvertible {
// MARK: - ProfileUserInfoTransformable // MARK: - ProfileUserInfoTransformable
// FIXME: #570, test user info encoding/decoding with JSONSerialization
extension ProfileAttributes: ProfileUserInfoTransformable { extension ProfileAttributes: ProfileUserInfoTransformable {
public var userInfo: [String: AnyHashable]? { public var userInfo: [String: AnyHashable]? {
do { do {

View File

@ -1,8 +1,8 @@
// //
// AppFeatureRequiring.swift // AppFeatureRequiring+Modules.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 11/17/24. // Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -26,38 +26,6 @@
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
public protocol AppFeatureRequiring {
var features: Set<AppFeature> { get }
}
// MARK: - Profile
extension Profile: AppFeatureRequiring {
public var features: Set<AppFeature> {
let builders = activeModules.compactMap { module in
guard let builder = module.moduleBuilder() else {
fatalError("Cannot produce ModuleBuilder from Module: \(module)")
}
return builder
}
return builders.features
}
}
extension Array: AppFeatureRequiring where Element == any ModuleBuilder {
public var features: Set<AppFeature> {
let requirements = compactMap { builder in
guard let requiring = builder as? AppFeatureRequiring else {
fatalError("ModuleBuilder does not implement AppFeatureRequiring: \(builder)")
}
return requiring
}
return Set(requirements.flatMap(\.features))
}
}
// MARK: - Modules
extension DNSModule.Builder: AppFeatureRequiring { extension DNSModule.Builder: AppFeatureRequiring {
public var features: Set<AppFeature> { public var features: Set<AppFeature> {
[.dns] [.dns]
@ -103,11 +71,3 @@ extension WireGuardModule.Builder: AppFeatureRequiring {
providerId?.features ?? [] providerId?.features ?? []
} }
} }
// MARK: - Providers
extension ProviderID: AppFeatureRequiring {
public var features: Set<AppFeature> {
self != .oeck ? [.providers] : []
}
}

View File

@ -0,0 +1,51 @@
//
// AppFeatureRequiring+Profile.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutKit
extension Profile: AppFeatureRequiring {
public var features: Set<AppFeature> {
let builders = activeModules.compactMap { module in
guard let builder = module.moduleBuilder() else {
fatalError("Cannot produce ModuleBuilder from Module: \(module)")
}
return builder
}
return builders.features
}
}
extension Array: AppFeatureRequiring where Element == any ModuleBuilder {
public var features: Set<AppFeature> {
let requirements = compactMap { builder in
guard let requiring = builder as? AppFeatureRequiring else {
fatalError("ModuleBuilder does not implement AppFeatureRequiring: \(builder)")
}
return requiring
}
return Set(requirements.flatMap(\.features))
}
}

View File

@ -0,0 +1,33 @@
//
// AppFeatureRequiring+Providers.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutKit
extension ProviderID: AppFeatureRequiring {
public var features: Set<AppFeature> {
self != .oeck ? [.providers] : []
}
}

View File

@ -1,5 +1,5 @@
// //
// PassepartoutConfiguration+Extensions.swift // PassepartoutConfiguration+Logging.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 10/4/24. // Created by Davide De Rosa on 10/4/24.

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
@_exported import CommonIAP
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit

View File

@ -25,7 +25,6 @@
import CommonUtils import CommonUtils
import Foundation import Foundation
import Kvitto
import PassepartoutKit import PassepartoutKit
public actor FallbackReceiptReader: AppReceiptReader { public actor FallbackReceiptReader: AppReceiptReader {

View File

@ -31,4 +31,16 @@ extension Bundle {
.deletingLastPathComponent() .deletingLastPathComponent()
.appendingPathComponent("receipt") // could be "sandboxReceipt" .appendingPathComponent("receipt") // could be "sandboxReceipt"
} }
public func unsafeDecode<T: Decodable>(_ type: T.Type, filename: String) -> T {
guard let jsonURL = url(forResource: filename, withExtension: "json") else {
fatalError("Unable to find \(filename).json in bundle")
}
do {
let data = try Data(contentsOf: jsonURL)
return try JSONDecoder().decode(type, from: data)
} catch {
fatalError("Unable to decode \(filename).json: \(error)")
}
}
} }

View File

@ -1,26 +1 @@
//
// Dummy.swift
// Passepartout
//
// Created by Davide De Rosa on 11/8/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation

View File

@ -29,7 +29,7 @@ import SwiftUI
extension ModuleBuilder where Self: ModuleViewProviding { extension ModuleBuilder where Self: ModuleViewProviding {
@MainActor @MainActor
func preview(title: String = "") -> some View { public func preview(title: String = "") -> some View {
NavigationStack { NavigationStack {
moduleView(with: ProfileEditor(modules: [self]), impl: nil) moduleView(with: ProfileEditor(modules: [self]), impl: nil)
.navigationTitle(title) .navigationTitle(title)
@ -38,7 +38,7 @@ extension ModuleBuilder where Self: ModuleViewProviding {
} }
@MainActor @MainActor
func preview<C: View>(with content: (ProfileEditor, Self) -> C) -> some View { public func preview<C: View>(with content: (ProfileEditor, Self) -> C) -> some View {
NavigationStack { NavigationStack {
content(ProfileEditor(modules: [self]), self) content(ProfileEditor(modules: [self]), self)
} }

View File

@ -0,0 +1,34 @@
//
// ModuleDraftEditing+UI.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
extension ModuleDraftEditing {
@MainActor
public var draft: Binding<Draft> {
editor[module]
}
}

View File

@ -1,5 +1,5 @@
// //
// ProfileManager+Extensions.swift // ProfileManager+Editing.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 9/3/24. // Created by Davide De Rosa on 9/3/24.
@ -29,7 +29,7 @@ import PassepartoutKit
@MainActor @MainActor
extension ProfileManager { extension ProfileManager {
func removeProfiles(at offsets: IndexSet) async { public func removeProfiles(at offsets: IndexSet) async {
let idsToRemove = headers let idsToRemove = headers
.enumerated() .enumerated()
.filter { .filter {

View File

@ -0,0 +1,50 @@
//
// TunnelInstallationProviding+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import Foundation
import PassepartoutKit
@MainActor
extension TunnelInstallationProviding {
public var installation: TunnelInstallation? {
guard let currentProfile = tunnel.currentProfile else {
return nil
}
guard let header = profileManager.headers.first(where: {
$0.id == currentProfile.id
}) else {
return nil
}
return TunnelInstallation(header: header, onDemand: currentProfile.onDemand)
}
public var currentProfile: Profile? {
guard let id = tunnel.currentProfile?.id else {
return nil
}
return profileManager.profile(withId: id)
}
}

View File

@ -34,8 +34,8 @@ extension AppContext {
public static func mock(withRegistry registry: Registry) -> AppContext { public static func mock(withRegistry registry: Registry) -> AppContext {
let iapManager = IAPManager( let iapManager = IAPManager(
inAppHelper: MockAppProductHelper(), inAppHelper: FakeAppProductHelper(),
receiptReader: MockAppReceiptReader(), receiptReader: FakeAppReceiptReader(),
unrestrictedFeatures: [ unrestrictedFeatures: [
.interactiveLogin, .interactiveLogin,
.onDemand .onDemand

View File

@ -26,18 +26,10 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
protocol ModuleDraftEditing { public protocol ModuleDraftEditing {
associatedtype Draft: ModuleBuilder associatedtype Draft: ModuleBuilder
var editor: ProfileEditor { get } var editor: ProfileEditor { get }
var module: Draft { get } var module: Draft { get }
} }
extension ModuleDraftEditing {
@MainActor
var draft: Binding<Draft> {
editor[module]
}
}

View File

@ -26,7 +26,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
protocol ModuleViewFactory: AnyObject { public protocol ModuleViewFactory: AnyObject {
associatedtype Content: View associatedtype Content: View
@MainActor @MainActor

View File

@ -26,7 +26,7 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
protocol ModuleViewProviding { public protocol ModuleViewProviding {
associatedtype Content: View associatedtype Content: View
@MainActor @MainActor

View File

@ -27,7 +27,7 @@ import CommonUtils
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
protocol ProviderEntityViewProviding { public protocol ProviderEntityViewProviding {
associatedtype EntityContent: View associatedtype EntityContent: View
@MainActor @MainActor

View File

@ -25,32 +25,9 @@
import CommonLibrary import CommonLibrary
import Foundation import Foundation
import PassepartoutKit
public protocol TunnelInstallationProviding { public protocol TunnelInstallationProviding {
var profileManager: ProfileManager { get } var profileManager: ProfileManager { get }
var tunnel: ExtendedTunnel { get } var tunnel: ExtendedTunnel { get }
} }
@MainActor
extension TunnelInstallationProviding {
public var installation: TunnelInstallation? {
guard let currentProfile = tunnel.currentProfile else {
return nil
}
guard let header = profileManager.headers.first(where: {
$0.id == currentProfile.id
}) else {
return nil
}
return TunnelInstallation(header: header, onDemand: currentProfile.onDemand)
}
public var currentProfile: Profile? {
guard let id = tunnel.currentProfile?.id else {
return nil
}
return profileManager.profile(withId: id)
}
}

View File

@ -23,13 +23,14 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
@testable import CommonIAP
@testable import CommonLibrary @testable import CommonLibrary
import CommonUtils import CommonUtils
import Foundation import Foundation
import XCTest import XCTest
final class IAPManagerTests: XCTestCase { final class IAPManagerTests: XCTestCase {
// private let inApp = MockAppProductHelper() // private let inApp = FakeAppProductHelper()
private let olderBuildNumber = 500 private let olderBuildNumber = 500
@ -44,7 +45,7 @@ extension IAPManagerTests {
// MARK: Build products // MARK: Build products
func test_givenBuildProducts_whenOlder_thenFullVersion() async { func test_givenBuildProducts_whenOlder_thenFullVersion() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: olderBuildNumber, identifiers: []) await reader.setReceipt(withBuild: olderBuildNumber, identifiers: [])
let sut = IAPManager(receiptReader: reader) { build in let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber { if build <= self.defaultBuildNumber {
@ -57,7 +58,7 @@ extension IAPManagerTests {
} }
func test_givenBuildProducts_whenNewer_thenFreeVersion() async { func test_givenBuildProducts_whenNewer_thenFreeVersion() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: newerBuildNumber, products: []) await reader.setReceipt(withBuild: newerBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) { build in let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber { if build <= self.defaultBuildNumber {
@ -72,7 +73,7 @@ extension IAPManagerTests {
// MARK: Eligibility // MARK: Eligibility
func test_givenPurchasedFeature_whenReloadReceipt_thenIsEligible() async { func test_givenPurchasedFeature_whenReloadReceipt_thenIsEligible() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features)) XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
@ -85,7 +86,7 @@ extension IAPManagerTests {
} }
func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async { func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [ await reader.setReceipt(withBuild: defaultBuildNumber, products: [
.Features.networkSettings .Features.networkSettings
]) ])
@ -101,7 +102,7 @@ extension IAPManagerTests {
} }
func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async { func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt( await reader.setReceipt(
withBuild: defaultBuildNumber, withBuild: defaultBuildNumber,
products: [.Full.allPlatforms], products: [.Full.allPlatforms],
@ -114,7 +115,7 @@ extension IAPManagerTests {
} }
func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async { func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: []) await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
@ -126,7 +127,7 @@ extension IAPManagerTests {
} }
func test_givenFreeVersion_thenIsNotEligibleForAppleTV() async { func test_givenFreeVersion_thenIsNotEligibleForAppleTV() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: []) await reader.setReceipt(withBuild: defaultBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
@ -135,7 +136,7 @@ extension IAPManagerTests {
} }
func test_givenFullV2Version_thenIsEligibleForAnyFeatureExceptExcluded() async { func test_givenFullV2Version_thenIsEligibleForAnyFeatureExceptExcluded() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.allPlatforms]) await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.allPlatforms])
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
@ -155,7 +156,7 @@ extension IAPManagerTests {
} }
func test_givenAppleTV_thenIsEligibleForAppleTV() async { func test_givenAppleTV_thenIsEligibleForAppleTV() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Features.appleTV]) await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Features.appleTV])
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
@ -164,7 +165,7 @@ extension IAPManagerTests {
} }
func test_givenPlatformVersion_thenIsFullVersionForPlatform() async { func test_givenPlatformVersion_thenIsFullVersionForPlatform() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
#if os(macOS) #if os(macOS)
@ -179,7 +180,7 @@ extension IAPManagerTests {
} }
func test_givenPlatformVersion_thenIsNotFullVersionForOtherPlatform() async { func test_givenPlatformVersion_thenIsNotFullVersionForOtherPlatform() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(receiptReader: reader) let sut = IAPManager(receiptReader: reader)
#if os(macOS) #if os(macOS)
@ -196,7 +197,7 @@ extension IAPManagerTests {
// MARK: App level // MARK: App level
func test_givenBetaApp_thenIsRestricted() async { func test_givenBetaApp_thenIsRestricted() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader) let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -204,7 +205,7 @@ extension IAPManagerTests {
} }
func test_givenBetaApp_thenIsNotEligibleForAllFeatures() async { func test_givenBetaApp_thenIsNotEligibleForAllFeatures() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader) let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -212,7 +213,7 @@ extension IAPManagerTests {
} }
func test_givenBetaApp_thenIsEligibleForUserLevelFeatures() async { func test_givenBetaApp_thenIsEligibleForUserLevelFeatures() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader) let sut = IAPManager(customUserLevel: .beta, receiptReader: reader)
let eligible = AppUserLevel.beta.features let eligible = AppUserLevel.beta.features
@ -222,7 +223,7 @@ extension IAPManagerTests {
} }
func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async { func test_givenBetaApp_thenIsEligibleForUnrestrictedFeature() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand]) let sut = IAPManager(customUserLevel: .beta, receiptReader: reader, unrestrictedFeatures: [.onDemand])
var eligible = AppUserLevel.beta.features var eligible = AppUserLevel.beta.features
@ -233,7 +234,7 @@ extension IAPManagerTests {
} }
func test_givenFullV2App_thenIsFullVersion() async { func test_givenFullV2App_thenIsFullVersion() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullV2, receiptReader: reader) let sut = IAPManager(customUserLevel: .fullV2, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -241,7 +242,7 @@ extension IAPManagerTests {
} }
func test_givenSubscriberApp_thenIsFullVersion() async { func test_givenSubscriberApp_thenIsFullVersion() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .subscriber, receiptReader: reader) let sut = IAPManager(customUserLevel: .subscriber, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -249,7 +250,7 @@ extension IAPManagerTests {
} }
func test_givenFullV2App_thenIsEligibleForAnyFeatureExceptExcluded() async { func test_givenFullV2App_thenIsEligibleForAnyFeatureExceptExcluded() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullV2, receiptReader: reader) let sut = IAPManager(customUserLevel: .fullV2, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -268,7 +269,7 @@ extension IAPManagerTests {
} }
func test_givenSubscriberApp_thenIsEligibleForAnyFeature() async { func test_givenSubscriberApp_thenIsEligibleForAnyFeature() async {
let reader = MockAppReceiptReader() let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .subscriber, receiptReader: reader) let sut = IAPManager(customUserLevel: .subscriber, receiptReader: reader)
await sut.reloadReceipt() await sut.reloadReceipt()
@ -288,7 +289,7 @@ private extension IAPManager {
) { ) {
self.init( self.init(
customUserLevel: customUserLevel, customUserLevel: customUserLevel,
inAppHelper: MockAppProductHelper(), inAppHelper: FakeAppProductHelper(),
receiptReader: receiptReader, receiptReader: receiptReader,
unrestrictedFeatures: unrestrictedFeatures, unrestrictedFeatures: unrestrictedFeatures,
productsAtBuild: productsAtBuild productsAtBuild: productsAtBuild