Separate AppContext for previews and UI testing (#961)

Clarify the use of contexts:

- **Production** (.shared)
- **Previews** (.mock → .forPreviews)
  - ONLY use it in UILibrary for, well, previews
  - This context has dumb profiles with UUIDs as names
  - Registry is fake
- **UI Tests** (.forUITesting)
  - Add new context for UI testing
  - Selected based on command line arguments
  - This context has mock data tuned for decent screenshots
  - Registry is real

Share the same InAppProcessor in .shared and .forTesting contexts
because the app behavior was inconsistent regarding e.g. in-app
purchases.
This commit is contained in:
Davide 2024-11-28 17:31:17 +01:00 committed by GitHub
parent 962361cb9f
commit 2a467e0c7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 251 additions and 139 deletions

View File

@ -16,6 +16,8 @@
0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */; };
0E483E842CE6501100584B32 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E822CE6501100584B32 /* Shared+App.swift */; };
0E60512C2CE5393C00F763D4 /* PassepartoutImplementations in Frameworks */ = {isa = PBXBuildFile; productRef = 0E60512B2CE5393C00F763D4 /* PassepartoutImplementations */; };
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */; };
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */; };
0E757F132CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E757F122CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift */; };
0E757F202CD0D22B006E13E1 /* PassepartoutLoginItem.app in Embed Login Item */ = {isa = PBXBuildFile; fileRef = 0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
0E757F232CD0D2BD006E13E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E757F212CD0D2B7006E13E1 /* AppDelegate.swift */; };
@ -134,6 +136,8 @@
0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+Tunnel.swift"; sourceTree = "<group>"; };
0E483E822CE6501100584B32 /* Shared+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+App.swift"; sourceTree = "<group>"; };
0E5DFDDC2CDB8F9100F2DE70 /* Passepartout.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.storekit; sourceTree = "<group>"; };
0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+Testing.swift"; sourceTree = "<group>"; };
0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileManager+Testing.swift"; sourceTree = "<group>"; };
0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PassepartoutLoginItem.app; sourceTree = BUILT_PRODUCTS_DIR; };
0E757F122CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassepartoutLoginItemApp.swift; sourceTree = "<group>"; };
0E757F182CD0CFFD006E13E1 /* LoginItem.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoginItem.entitlements; sourceTree = "<group>"; };
@ -277,6 +281,15 @@
path = Resources;
sourceTree = "<group>";
};
0E6EEEE62CF8CB090076E2B0 /* Testing */ = {
isa = PBXGroup;
children = (
0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */,
0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */,
);
path = Testing;
sourceTree = "<group>";
};
0E757F112CD0CFFC006E13E1 /* LoginItem */ = {
isa = PBXGroup;
children = (
@ -323,6 +336,7 @@
0E7E3D612B9345FD002BBDB4 /* Shared */ = {
isa = PBXGroup;
children = (
0E6EEEE62CF8CB090076E2B0 /* Testing */,
0EC797402B9378E000C093B7 /* AppContext+Shared.swift */,
0EC797412B9378E000C093B7 /* Shared.swift */,
0E483E822CE6501100584B32 /* Shared+App.swift */,
@ -673,6 +687,8 @@
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */,
0EC797422B9378E000C093B7 /* AppContext+Shared.swift in Sources */,
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */,
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */,
0E483E842CE6501100584B32 /* Shared+App.swift in Sources */,
0EC797432B9378E000C093B7 /* Shared.swift in Sources */,
);

View File

@ -32,9 +32,9 @@ import UITesting
@MainActor
final class AppDelegate: NSObject {
let context: AppContext = {
guard !AppCommandLine.contains(.uiTesting) else {
if AppCommandLine.contains(.uiTesting) {
pp_log(.app, .info, "UI tests: mock AppContext")
return .mock(withRegistry: .shared)
return .forUITesting(withRegistry: .shared)
}
return .shared
}()

View File

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

View File

@ -165,8 +165,8 @@ extension AboutCoordinator {
#Preview {
AboutCoordinator(
profileManager: .mock,
tunnel: .mock
profileManager: .forPreviews,
tunnel: .forPreviews
)
.withMockEnvironment()
}

View File

@ -270,7 +270,7 @@ extension AppCoordinator {
}
var overriddenLayout: ProfilesLayout {
guard !isUITesting else {
if isUITesting {
return isBigDevice ? .grid : .list
}
return layout
@ -306,8 +306,8 @@ extension AppCoordinator {
#Preview {
AppCoordinator(
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
registry: Registry()
)
.withMockEnvironment()

View File

@ -110,7 +110,7 @@ private extension AppToolbar {
Text("AppToolbar")
.toolbar {
AppToolbar(
profileManager: .mock,
profileManager: .forPreviews,
registry: Registry(),
layout: .constant(.list),
isImporting: .constant(false),

View File

@ -123,7 +123,7 @@ private extension InstalledProfileView {
style: .installedProfile,
profileManager: profileManager,
tunnel: tunnel,
preview: .init(profile ?? .mock),
preview: .init(profile ?? .forPreviews),
interactiveManager: interactiveManager,
errorHandler: errorHandler,
flow: flow
@ -298,9 +298,9 @@ private struct HeaderView: View {
var body: some View {
InstalledProfileView(
layout: layout,
profileManager: .mock,
profile: .mock,
tunnel: .mock,
profileManager: .forPreviews,
profile: .forPreviews,
tunnel: .forPreviews,
interactiveManager: InteractiveManager(),
errorHandler: .default(),
nextProfileId: .constant(nil)
@ -313,9 +313,9 @@ private struct ContentView: View {
ForEach(0..<3) { _ in
ProfileRowView(
style: .full,
profileManager: .mock,
tunnel: .mock,
preview: .init(.mock),
profileManager: .forPreviews,
tunnel: .forPreviews,
preview: .init(.forPreviews),
interactiveManager: InteractiveManager(),
errorHandler: .default(),
nextProfileId: .constant(nil),

View File

@ -70,7 +70,7 @@ struct OnboardingModifier: ViewModifier {
private extension OnboardingModifier {
func advance() {
guard !isUITesting else {
if isUITesting {
pp_log(.app, .info, "UI tests: skip onboarding")
return
}

View File

@ -67,13 +67,13 @@ struct ProfileCardView: View {
Section {
ProfileCardView(
style: .compact,
preview: .init(.mock)
preview: .init(.forPreviews)
)
}
Section {
ProfileCardView(
style: .full,
preview: .init(.mock)
preview: .init(.forPreviews)
)
}
}

View File

@ -157,8 +157,8 @@ private struct PreviewView: View {
NavigationStack {
ProfileContainerView(
layout: layout,
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
registry: Registry(),
isImporting: .constant(false),
errorHandler: .default()

View File

@ -158,9 +158,9 @@ private extension ProfileContextMenu {
Menu("Menu") {
ProfileContextMenu(
style: .installedProfile,
profileManager: .mock,
tunnel: .mock,
preview: .init(.mock),
profileManager: .forPreviews,
tunnel: .forPreviews,
preview: .init(.forPreviews),
interactiveManager: InteractiveManager(),
errorHandler: .default()
)

View File

@ -150,8 +150,8 @@ private extension ProfileGridView {
#Preview {
ProfileGridView(
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
interactiveManager: InteractiveManager(),
errorHandler: .default()
)

View File

@ -149,8 +149,8 @@ private extension ProfileListView {
#Preview {
ProfileListView(
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
interactiveManager: InteractiveManager(),
errorHandler: .default()
)

View File

@ -205,14 +205,14 @@ private extension ProfileRowView {
// MARK: - Previews
#Preview {
let profile: Profile = .mock
let profileManager: ProfileManager = .mock
let profile: Profile = .forPreviews
let profileManager: ProfileManager = .forPreviews
return Form {
ProfileRowView(
style: .compact,
profileManager: profileManager,
tunnel: .mock,
tunnel: .forPreviews,
preview: .init(profile),
interactiveManager: InteractiveManager(),
errorHandler: .default(),

View File

@ -187,7 +187,7 @@ private extension DiagnosticsView {
}
#Preview {
DiagnosticsView(profileManager: .mock, tunnel: .mock) {
DiagnosticsView(profileManager: .forPreviews, tunnel: .forPreviews) {
[
.init(date: Date(), url: URL(string: "http://one.com")!),
.init(date: Date().addingTimeInterval(-60), url: URL(string: "http://two.com")!),

View File

@ -60,8 +60,8 @@ private extension ModuleDetailView {
#Preview {
ModuleDetailView(
profileEditor: ProfileEditor(profile: .mock),
moduleId: Profile.mock.modules.first?.id,
profileEditor: ProfileEditor(profile: .forPreviews),
moduleId: Profile.forPreviews.modules.first?.id,
moduleViewFactory: DefaultModuleViewFactory(registry: Registry())
)
.withMockEnvironment()

View File

@ -164,7 +164,7 @@ private extension ProfileCoordinator {
#Preview {
ProfileCoordinator(
profileManager: .mock,
profileManager: .forPreviews,
profileEditor: ProfileEditor(profile: .newMockProfile()),
initialModuleId: nil,
registry: Registry(),

View File

@ -147,7 +147,7 @@ private extension ModuleListView {
#Preview {
ModuleListView(
profileEditor: ProfileEditor(profile: .mock),
profileEditor: ProfileEditor(profile: .forPreviews),
selectedModuleId: .constant(nil),
errorModuleIds: .constant([]),
paywallReason: .constant(nil)

View File

@ -136,8 +136,8 @@ private extension AppCoordinator {
#Preview {
AppCoordinator(
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
registry: Registry()
)
.withMockEnvironment()

View File

@ -231,7 +231,7 @@ private extension ActiveProfileView {
.frame(maxWidth: .infinity)
}
.task {
try? await ProviderManager.mock.fetchIndex(from: [API.bundled])
try? await ProviderManager.forPreviews.fetchIndex(from: [API.bundled])
}
}
@ -247,7 +247,7 @@ private struct ContentPreview: View {
var body: some View {
ActiveProfileView(
profile: profile,
tunnel: .mock,
tunnel: .forPreviews,
isSwitching: $isSwitching,
focusedField: $focusedField,
interactiveManager: InteractiveManager(),

View File

@ -115,7 +115,7 @@ private extension ProfileListView {
// MARK: - Previews
#Preview("List") {
ContentPreview(profileManager: .mock)
ContentPreview(profileManager: .forPreviews)
}
#Preview("Empty") {
@ -131,7 +131,7 @@ private struct ContentPreview: View {
var body: some View {
ProfileListView(
profileManager: profileManager,
tunnel: .mock,
tunnel: .forPreviews,
focusedField: $focusedField,
interactiveManager: InteractiveManager(),
errorHandler: .default()

View File

@ -190,8 +190,8 @@ private extension ProfileView {
#Preview("List") {
ProfileView(
profileManager: .mock,
tunnel: .mock,
profileManager: .forPreviews,
tunnel: .forPreviews,
errorHandler: .default(),
showsSidePanel: true
)
@ -201,7 +201,7 @@ private extension ProfileView {
#Preview("Empty") {
ProfileView(
profileManager: ProfileManager(profiles: []),
tunnel: .mock,
tunnel: .forPreviews,
errorHandler: .default(),
showsSidePanel: true
)

View File

@ -145,7 +145,7 @@ private struct DetailView: View {
// MARK: -
#Preview {
SettingsView(tunnel: .mock)
SettingsView(tunnel: .forPreviews)
.themeNavigationStack()
.withMockEnvironment()
}

View File

@ -174,7 +174,7 @@ extension ProfileManager {
return
}
requiredFeatures = allProfiles.reduce(into: [:]) {
guard let ineligible = processor.verify($1.value), !ineligible.isEmpty else {
guard let ineligible = processor.requiredFeatures($1.value), !ineligible.isEmpty else {
return
}
$0[$1.key] = ineligible

View File

@ -35,7 +35,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
private nonisolated let _preview: (Profile) -> ProfilePreview
private nonisolated let _verify: (IAPManager, Profile) -> Set<AppFeature>?
private nonisolated let _requiredFeatures: (IAPManager, Profile) -> Set<AppFeature>?
private nonisolated let _willRebuild: (IAPManager, Profile.Builder) throws -> Profile.Builder
@ -46,7 +46,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
title: @escaping (Profile) -> String,
isIncluded: @escaping (IAPManager, Profile) -> Bool,
preview: @escaping (Profile) -> ProfilePreview,
verify: @escaping (IAPManager, Profile) -> Set<AppFeature>?,
requiredFeatures: @escaping (IAPManager, Profile) -> Set<AppFeature>?,
willRebuild: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
willInstall: @escaping (IAPManager, Profile) throws -> Profile
) {
@ -54,7 +54,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
_title = title
_isIncluded = isIncluded
_preview = preview
_verify = verify
_requiredFeatures = requiredFeatures
_willRebuild = willRebuild
_willInstall = willInstall
}
@ -75,8 +75,8 @@ extension InAppProcessor: ProfileProcessor {
_preview(profile)
}
public func verify(_ profile: Profile) -> Set<AppFeature>? {
_verify(iapManager, profile)
public func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
_requiredFeatures(iapManager, profile)
}
public func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {

View File

@ -31,7 +31,7 @@ public protocol ProfileProcessor {
func preview(from profile: Profile) -> ProfilePreview
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>?
func verify(_ profile: Profile) -> Set<AppFeature>?
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
}

View File

@ -36,8 +36,8 @@ extension View {
public func withMockEnvironment() -> some View {
task {
try? await AppContext.mock.profileManager.observeLocal()
try? await AppContext.forPreviews.profileManager.observeLocal()
}
.withEnvironment(from: .mock, theme: Theme())
.withEnvironment(from: .forPreviews, theme: Theme())
}
}

View File

@ -1,5 +1,5 @@
//
// AppContext+Mock.swift
// AppContext+Previews.swift
// Passepartout
//
// Created by Davide De Rosa on 6/22/24.
@ -23,25 +23,18 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Combine
import CommonLibrary
import CommonUtils
import Foundation
import PassepartoutKit
extension AppContext {
public static let mock: AppContext = .mock(withRegistry: Registry())
public static func mock(withRegistry registry: Registry) -> AppContext {
public static let forPreviews: AppContext = {
let iapManager = IAPManager(
customUserLevel: .subscriber,
inAppHelper: FakeAppProductHelper(),
receiptReader: FakeAppReceiptReader(),
betaChecker: TestFlightChecker(),
unrestrictedFeatures: [
.interactiveLogin,
.onDemand
],
productsAtBuild: { _ in
[]
}
@ -57,17 +50,23 @@ extension AppContext {
preview: {
$0.localizedPreview
},
verify: { _, _ in
requiredFeatures: { _, _ in
nil
},
willRebuild: { _, builder in
builder
},
willInstall: { _, profile in
try profile.withProviderModules()
profile
}
)
let profileManager: ProfileManager = .mock(withRegistry: registry, processor: processor)
let profileManager = {
let profiles: [Profile] = (0..<20)
.reduce(into: []) { list, _ in
list.append(.newMockProfile())
}
return ProfileManager(profiles: profiles)
}()
let tunnelEnvironment = InMemoryEnvironment()
let tunnel = ExtendedTunnel(
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: tunnelEnvironment)),
@ -84,35 +83,35 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
registry: registry,
registry: Registry(),
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
)
}
}()
}
// MARK: - Shortcuts
extension IAPManager {
public static var mock: IAPManager {
AppContext.mock.iapManager
public static var forPreviews: IAPManager {
AppContext.forPreviews.iapManager
}
}
extension ProfileManager {
public static var mock: ProfileManager {
AppContext.mock.profileManager
public static var forPreviews: ProfileManager {
AppContext.forPreviews.profileManager
}
}
extension ExtendedTunnel {
public static var mock: ExtendedTunnel {
AppContext.mock.tunnel
public static var forPreviews: ExtendedTunnel {
AppContext.forPreviews.tunnel
}
}
extension ProviderManager {
public static var mock: ProviderManager {
AppContext.mock.providerManager
public static var forPreviews: ProviderManager {
AppContext.forPreviews.providerManager
}
}

View File

@ -1,5 +1,5 @@
//
// Profile+Mock.swift
// Profile+Previews.swift
// Passepartout
//
// Created by Davide De Rosa on 11/4/24.
@ -27,7 +27,7 @@ import Foundation
import PassepartoutKit
extension Profile {
public static let mock: Profile = {
public static let forPreviews: Profile = {
var profile = Profile.Builder()
profile.name = "Mock profile"
do {
@ -63,7 +63,7 @@ extension Profile {
public static func newMockProfile(withName name: String? = nil) -> Profile {
do {
var copy = mock.builder(withNewId: true)
var copy = forPreviews.builder(withNewId: true)
copy.name = name ?? String(copy.id.uuidString.prefix(8))
return try copy.tryBuild()
} catch {

View File

@ -76,9 +76,9 @@ private extension ConnectionStatusText {
}
#Preview("Connected") {
ConnectionStatusText(tunnel: .mock)
ConnectionStatusText(tunnel: .forPreviews)
.task {
try? await ExtendedTunnel.mock.connect(with: .mock)
try? await ExtendedTunnel.forPreviews.connect(with: .forPreviews)
}
.frame(width: 100, height: 100)
.withMockEnvironment()
@ -95,9 +95,9 @@ private extension ConnectionStatusText {
} catch {
fatalError()
}
return ConnectionStatusText(tunnel: .mock)
return ConnectionStatusText(tunnel: .forPreviews)
.task {
try? await ExtendedTunnel.mock.connect(with: profile)
try? await ExtendedTunnel.forPreviews.connect(with: profile)
}
.frame(width: 100, height: 100)
.withMockEnvironment()

View File

@ -30,14 +30,14 @@ import PassepartoutKit
final class MockProfileProcessor: ProfileProcessor {
var isIncludedCount = 0
var willRebuildCount = 0
var verifyCount = 0
var isIncludedBlock: (Profile) -> Bool = { _ in true }
var requiredFeaturesCount = 0
var requiredFeatures: Set<AppFeature>?
var willRebuildCount = 0
func title(for profile: Profile) -> String {
profile.name
}
@ -51,13 +51,13 @@ final class MockProfileProcessor: ProfileProcessor {
ProfilePreview(profile)
}
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
requiredFeaturesCount += 1
return requiredFeatures
}
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
willRebuildCount += 1
return builder
}
func verify(_ profile: Profile) -> Set<AppFeature>? {
verifyCount += 1
return requiredFeatures
}
}

View File

@ -100,8 +100,8 @@ extension ProfileManagerTests {
XCTAssertTrue(sut.isReady)
XCTAssertEqual(processor.isIncludedCount, 1)
XCTAssertEqual(processor.requiredFeaturesCount, 1)
XCTAssertEqual(processor.willRebuildCount, 0)
XCTAssertEqual(processor.verifyCount, 1)
XCTAssertEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
}

View File

@ -38,6 +38,10 @@ import UITesting
extension AppContext {
static let shared: AppContext = {
let iapManager: IAPManager = .sharedForApp
let processor = InAppProcessor.shared(iapManager) {
$0.localizedPreview
}
// MARK: ProfileManager
@ -65,7 +69,7 @@ extension AppContext {
backupRepository: Configuration.ProfileManager.backupProfileRepository,
remoteRepositoryBlock: remoteRepositoryBlock,
mirrorsRemoteRepository: Configuration.ProfileManager.mirrorsRemoteRepository,
processor: IAPManager.sharedProcessor
processor: processor
)
}()
@ -74,7 +78,7 @@ extension AppContext {
let tunnel = ExtendedTunnel(
tunnel: Tunnel(strategy: Configuration.ExtendedTunnel.strategy),
environment: .shared,
processor: IAPManager.sharedProcessor,
processor: processor,
interval: Constants.shared.tunnel.refreshInterval
)
@ -118,7 +122,7 @@ extension AppContext {
let migrationManager = MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation)
return AppContext(
iapManager: .sharedForApp,
iapManager: iapManager,
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,

View File

@ -37,44 +37,6 @@ extension IAPManager {
betaChecker: Configuration.IAPManager.betaChecker,
productsAtBuild: Configuration.IAPManager.productsAtBuild
)
static let sharedProcessor = InAppProcessor(
iapManager: sharedForApp,
title: {
Configuration.ProfileManager.sharedTitle($0)
},
isIncluded: {
Configuration.ProfileManager.isIncluded($0, $1)
},
preview: {
$0.localizedPreview
},
verify: { iap, profile in
do {
try iap.verify(profile)
return nil
} catch AppError.ineligibleProfile(let requiredFeatures) {
return requiredFeatures
} catch {
return nil
}
},
willRebuild: { _, builder in
builder
},
willInstall: { iap, profile in
try iap.verify(profile)
// validate provider modules
do {
_ = try profile.withProviderModules()
return profile
} catch {
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error
}
}
)
}
// MARK: - Configuration
@ -90,7 +52,7 @@ private extension Configuration.IAPManager {
@MainActor
static let simulatedInAppHelper: any AppProductHelper = {
guard !AppCommandLine.contains(.fakeIAP) else {
if AppCommandLine.contains(.fakeIAP) {
return FakeAppProductHelper()
}
return inAppHelper
@ -98,7 +60,7 @@ private extension Configuration.IAPManager {
@MainActor
static var simulatedAppReceiptReader: AppReceiptReader {
guard !AppCommandLine.contains(.fakeIAP) else {
if AppCommandLine.contains(.fakeIAP) {
guard let mockHelper = inAppHelper as? FakeAppProductHelper else {
fatalError("When .isFakeIAP, productHelper is expected to be MockAppProductHelper")
}

View File

@ -89,6 +89,50 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
}
}
// MARK: InAppProcessor
extension InAppProcessor {
@MainActor
static func shared(_ iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
InAppProcessor(
iapManager: iapManager,
title: {
Configuration.ProfileManager.sharedTitle($0)
},
isIncluded: {
Configuration.ProfileManager.isIncluded($0, $1)
},
preview: preview,
requiredFeatures: { iap, profile in
do {
try iap.verify(profile)
return nil
} catch AppError.ineligibleProfile(let requiredFeatures) {
return requiredFeatures
} catch {
return nil
}
},
willRebuild: { _, builder in
builder
},
willInstall: { iap, profile in
try iap.verify(profile)
// validate provider modules
do {
_ = try profile.withProviderModules()
return profile
} catch {
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error
}
}
)
}
}
// MARK: - Configuration
enum Configuration {

View File

@ -0,0 +1,73 @@
//
// AppContext+Testing.swift
// Passepartout
//
// Created by Davide De Rosa on 11/28/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 CommonUtils
import Foundation
import PassepartoutKit
import UILibrary
extension AppContext {
static func forUITesting(withRegistry registry: Registry) -> AppContext {
let iapManager = IAPManager(
customUserLevel: .subscriber,
inAppHelper: FakeAppProductHelper(),
receiptReader: FakeAppReceiptReader(),
betaChecker: TestFlightChecker(),
productsAtBuild: { _ in
[]
}
)
let processor = InAppProcessor.shared(iapManager) {
$0.localizedPreview
}
let profileManager: ProfileManager = .forTesting(
withRegistry: .shared,
processor: processor
)
let tunnelEnvironment = InMemoryEnvironment()
let tunnel = ExtendedTunnel(
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: tunnelEnvironment)),
environment: tunnelEnvironment,
processor: processor,
interval: Constants.shared.tunnel.refreshInterval
)
let providerManager = ProviderManager(
repository: InMemoryProviderRepository()
)
let migrationManager = MigrationManager()
return AppContext(
iapManager: iapManager,
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
registry: registry,
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
)
}
}

View File

@ -1,5 +1,5 @@
//
// ProfileManager+Mock.swift
// ProfileManager+Testing.swift
// Passepartout
//
// Created by Davide De Rosa on 11/28/24.
@ -28,7 +28,7 @@ import Foundation
import PassepartoutKit
extension ProfileManager {
public static func mock(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager {
public static func forTesting(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager {
let repository = InMemoryProfileRepository()
let remoteRepository = InMemoryProfileRepository()
let manager = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in