Revisit overuse of EnvironmentObject (#794)

The biggest issue is the hidden and scattered use of both Tunnel and
ConnectionObserver. Only use the latter, and rename it to ExtendedTunnel
for being now a full wrapper around Tunnel (e.g. for .connectionStatus).

In general, restrict the use of EnvironmentObject to:

- Theme
- IAPManager
- ProfileProcessor
- ProviderManager

Always be explicit about:

- ProfileManager
- ExtendedTunnel

Contextually, move some UI entities to the base AppUI target.
This commit is contained in:
Davide 2024-11-01 09:47:50 +01:00 committed by GitHub
parent 33d238270e
commit 590b2790fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 200 additions and 223 deletions

View File

@ -83,10 +83,14 @@ extension PassepartoutApp {
.withEnvironment(from: context, theme: theme)
}
MenuBarExtra {
AppMenu()
AppMenu(
profileManager: context.profileManager,
profileProcessor: context.profileProcessor,
tunnel: context.tunnel
)
.withEnvironment(from: context, theme: theme)
} label: {
AppMenuImage(connectionObserver: context.connectionObserver)
AppMenuImage(tunnel: context.tunnel)
.environmentObject(theme)
}
}

View File

@ -38,12 +38,10 @@ public final class AppContext: ObservableObject {
public let profileProcessor: ProfileProcessor
public let tunnel: Tunnel
public let tunnel: ExtendedTunnel
public let tunnelEnvironment: TunnelEnvironment
public let connectionObserver: ConnectionObserver
public let registry: Registry
public let providerManager: ProviderManager
@ -65,9 +63,8 @@ public final class AppContext: ObservableObject {
self.iapManager = iapManager
self.profileManager = profileManager
self.profileProcessor = profileProcessor
self.tunnel = tunnel
self.tunnelEnvironment = tunnelEnvironment
connectionObserver = ConnectionObserver(
self.tunnel = ExtendedTunnel(
tunnel: tunnel,
environment: tunnelEnvironment,
interval: constants.tunnel.refreshInterval
@ -79,7 +76,7 @@ public final class AppContext: ObservableObject {
Task {
await iapManager.reloadReceipt()
connectionObserver.observeObjects()
self.tunnel.observeObjects()
profileManager.observeObjects()
observeObjects()
}

View File

@ -1,5 +1,5 @@
//
// ConnectionObserver.swift
// ExtendedTunnel.swift
// Passepartout
//
// Created by Davide De Rosa on 9/7/24.
@ -29,8 +29,8 @@ import Foundation
import PassepartoutKit
@MainActor
public final class ConnectionObserver: ObservableObject {
public let tunnel: Tunnel
public final class ExtendedTunnel: ObservableObject {
private let tunnel: Tunnel
private let environment: TunnelEnvironment
@ -40,14 +40,10 @@ public final class ConnectionObserver: ObservableObject {
environment.environmentValue(forKey: key)
}
public var connectionStatus: ConnectionStatus? {
value(forKey: TunnelEnvironmentKeys.connectionStatus)
}
@Published
public private(set) var lastErrorCode: PassepartoutError.Code? {
didSet {
pp_log(.app, .info, "ConnectionObserver.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")")
pp_log(.app, .info, "ExtendedTunnel.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")")
}
}
@ -103,3 +99,63 @@ public final class ConnectionObserver: ObservableObject {
.store(in: &subscriptions)
}
}
extension ExtendedTunnel {
public var status: TunnelStatus {
tunnel.status
}
public var connectionStatus: TunnelStatus {
var status = tunnel.status
if status == .active, let environmentConnectionStatus {
if environmentConnectionStatus == .connected {
status = .active
} else {
status = .activating
}
}
return status
}
private var environmentConnectionStatus: ConnectionStatus? {
value(forKey: TunnelEnvironmentKeys.connectionStatus)
}
}
extension ExtendedTunnel {
public var currentProfile: TunnelCurrentProfile? {
tunnel.currentProfile
}
public func prepare(purge: Bool) async throws {
try await tunnel.prepare(purge: purge)
}
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await tunnel.install(newProfile, connect: false, title: processor.title)
}
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await tunnel.install(newProfile, connect: true, title: processor.title)
}
public func disconnect() async throws {
try await tunnel.disconnect()
}
public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await tunnel.sendMessage(.localLog(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel
))
switch output {
case .debugLog(let log):
return log.lines.map(parameters.formatter.formattedLine)
default:
return []
}
}
}

View File

@ -27,23 +27,26 @@ import Foundation
import PassepartoutKit
@MainActor
final class InteractiveManager: ObservableObject {
typealias CompletionBlock = (Profile) async throws -> Void
public final class InteractiveManager: ObservableObject {
public typealias CompletionBlock = (Profile) async throws -> Void
@Published
var isPresented = false
public var isPresented = false
private(set) var editor = ProfileEditor()
public private(set) var editor = ProfileEditor()
private var onComplete: CompletionBlock?
func present(with profile: Profile, onComplete: CompletionBlock?) {
public init() {
}
public func present(with profile: Profile, onComplete: CompletionBlock?) {
editor = ProfileEditor(profile: profile)
self.onComplete = onComplete
isPresented = true
}
func complete() async throws {
public func complete() async throws {
isPresented = false
let newProfile = try editor.build()
try await onComplete?(newProfile)

View File

@ -1,55 +0,0 @@
//
// Tunnel+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 8/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 CommonLibrary
import Foundation
import PassepartoutKit
@MainActor
extension Tunnel {
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: false, title: processor.title)
}
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: true, title: processor.title)
}
public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await sendMessage(.localLog(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel
))
switch output {
case .debugLog(let log):
return log.lines.map(parameters.formatter.formattedLine)
default:
return []
}
}
}

View File

@ -30,12 +30,9 @@ import SwiftUI
extension View {
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
environmentObject(theme)
.environmentObject(context.connectionObserver)
.environmentObject(context.iapManager)
.environmentObject(context.profileManager)
.environmentObject(context.profileProcessor)
.environmentObject(context.providerManager)
.environmentObject(context.tunnel)
}
public func withMockEnvironment() -> some View {

View File

@ -88,18 +88,12 @@ extension ProfileProcessor {
}
}
extension Tunnel {
public static var mock: Tunnel {
extension ExtendedTunnel {
public static var mock: ExtendedTunnel {
AppContext.mock.tunnel
}
}
extension ConnectionObserver {
public static var mock: ConnectionObserver {
AppContext.mock.connectionObserver
}
}
extension ProviderManager {
public static var mock: ProviderManager {
AppContext.mock.providerManager

View File

@ -30,7 +30,7 @@ import PassepartoutKit
public protocol AppCoordinatorConforming {
init(
profileManager: ProfileManager,
tunnel: Tunnel,
tunnel: ExtendedTunnel,
registry: Registry
)
}

View File

@ -1,46 +0,0 @@
//
// TunnelContextProviding.swift
// Passepartout
//
// Created by Davide De Rosa on 9/5/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
public protocol TunnelContextProviding {
var connectionObserver: ConnectionObserver { get }
}
@MainActor
extension TunnelContextProviding {
public var tunnelConnectionStatus: TunnelStatus {
var status = connectionObserver.tunnel.status
if status == .active, let connectionStatus = connectionObserver.connectionStatus {
if connectionStatus == .connected {
status = .active
} else {
status = .activating
}
}
return status
}
}

View File

@ -30,7 +30,7 @@ import PassepartoutKit
public protocol TunnelInstallationProviding {
var profileManager: ProfileManager { get }
var tunnel: Tunnel { get }
var tunnel: ExtendedTunnel { get }
}
@MainActor

View File

@ -27,32 +27,34 @@ import Foundation
import PassepartoutKit
import SwiftUI
struct ConnectionStatusView: View, TunnelContextProviding, ThemeProviding {
public struct ConnectionStatusView: View, ThemeProviding {
@EnvironmentObject
var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
public var theme: Theme
@ObservedObject
var tunnel: Tunnel
private var tunnel: ExtendedTunnel
var body: some View {
public init(tunnel: ExtendedTunnel) {
self.tunnel = tunnel
}
public var body: some View {
Text(statusDescription)
.foregroundStyle(tunnelStatusColor)
.font(.headline)
.foregroundStyle(tunnel.statusColor(theme))
}
}
private extension ConnectionStatusView {
var statusDescription: String {
if let lastErrorCode = connectionObserver.lastErrorCode {
if let lastErrorCode = tunnel.lastErrorCode {
return lastErrorCode.localizedDescription
}
let status = tunnelConnectionStatus
let status = tunnel.connectionStatus
switch status {
case .active:
if let dataCount = connectionObserver.dataCount {
if let dataCount = tunnel.dataCount {
let down = dataCount.received.descriptionAsDataUnit
let up = dataCount.sent.descriptionAsDataUnit
return "\(down)\(up)"
@ -75,7 +77,7 @@ private extension ConnectionStatusView {
#Preview("Connected") {
ConnectionStatusView(tunnel: .mock)
.task {
try? await Tunnel.mock.connect(with: .mock, processor: .mock)
try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
}
.frame(width: 100, height: 100)
.withMockEnvironment()
@ -94,7 +96,7 @@ private extension ConnectionStatusView {
}
return ConnectionStatusView(tunnel: .mock)
.task {
try? await Tunnel.mock.connect(with: profile, processor: .mock)
try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
}
.frame(width: 100, height: 100)
.withMockEnvironment()

View File

@ -1,5 +1,5 @@
//
// TunnelContextProviding+Theme.swift
// ExtendedTunnel+Theme.swift
// Passepartout
//
// Created by Davide De Rosa on 9/6/24.
@ -26,11 +26,12 @@
import PassepartoutKit
import SwiftUI
@MainActor
extension TunnelContextProviding where Self: ThemeProviding {
var tunnelStatusColor: Color {
if connectionObserver.lastErrorCode != nil {
switch connectionObserver.tunnel.status {
extension ExtendedTunnel {
@MainActor
public func statusColor(_ theme: Theme) -> Color {
if lastErrorCode != nil {
switch status {
case .inactive:
return theme.inactiveColor
@ -38,7 +39,7 @@ extension TunnelContextProviding where Self: ThemeProviding {
return theme.errorColor
}
}
switch tunnelConnectionStatus {
switch connectionStatus {
case .active:
return theme.activeColor

View File

@ -27,18 +27,15 @@ import PassepartoutKit
import SwiftUI
import UtilsLibrary
struct TunnelToggleButton<Label>: View, TunnelContextProviding, ThemeProviding where Label: View {
enum Style {
public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View {
public enum Style {
case plain
case color
}
@EnvironmentObject
var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
public var theme: Theme
@EnvironmentObject
private var iapManager: IAPManager
@ -46,25 +43,45 @@ struct TunnelToggleButton<Label>: View, TunnelContextProviding, ThemeProviding w
@EnvironmentObject
private var profileProcessor: ProfileProcessor
var style: Style = .plain
private let style: Style
@ObservedObject
var tunnel: Tunnel
private var tunnel: ExtendedTunnel
let profile: Profile?
private let profile: Profile?
@Binding
var nextProfileId: Profile.ID?
private var nextProfileId: Profile.ID?
let interactiveManager: InteractiveManager
private let interactiveManager: InteractiveManager
let errorHandler: ErrorHandler
private let errorHandler: ErrorHandler
var onProviderEntityRequired: ((Profile) -> Void)?
private let onProviderEntityRequired: ((Profile) -> Void)?
let label: (Bool) -> Label
private let label: (Bool) -> Label
var body: some View {
public init(
style: Style = .plain,
tunnel: ExtendedTunnel,
profile: Profile?,
nextProfileId: Binding<Profile.ID?>,
interactiveManager: InteractiveManager,
errorHandler: ErrorHandler,
onProviderEntityRequired: ((Profile) -> Void)? = nil,
label: @escaping (Bool) -> Label
) {
self.style = style
self.tunnel = tunnel
self.profile = profile
_nextProfileId = nextProfileId
self.interactiveManager = interactiveManager
self.errorHandler = errorHandler
self.onProviderEntityRequired = onProviderEntityRequired
self.label = label
}
public var body: some View {
Button(action: tryPerform) {
label(canConnect)
}
@ -92,7 +109,7 @@ private extension TunnelToggleButton {
return .primary
case .color:
return tunnelStatusColor
return tunnel.statusColor(theme)
}
}
}

View File

@ -39,7 +39,7 @@ extension Issue {
let purchasedProducts: Set<AppProduct>
let tunnel: Tunnel
let tunnel: ExtendedTunnel
let urlForTunnelLog: URL

View File

@ -31,7 +31,7 @@ import UtilsLibrary
struct InstalledProfileView: View, Routable {
@EnvironmentObject
var theme: Theme
private var theme: Theme
let layout: ProfilesLayout
@ -39,7 +39,8 @@ struct InstalledProfileView: View, Routable {
let profile: Profile?
let tunnel: Tunnel
@ObservedObject
var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager

View File

@ -31,7 +31,7 @@ import UtilsLibrary
struct ProfileContextMenu: View, Routable {
let profileManager: ProfileManager
let tunnel: Tunnel
let tunnel: ExtendedTunnel
let header: ProfileHeader

View File

@ -28,21 +28,18 @@ import PassepartoutKit
import SwiftUI
import UtilsLibrary
struct ProfileRowView: View, Routable, TunnelContextProviding {
struct ProfileRowView: View, Routable {
@EnvironmentObject
private var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
let style: ProfileCardView.Style
@ObservedObject
var profileManager: ProfileManager
@ObservedObject
var tunnel: Tunnel
var tunnel: ExtendedTunnel
let header: ProfileHeader
@ -116,7 +113,7 @@ private extension ProfileRowView {
}
var statusImage: Theme.ImageName {
switch tunnelConnectionStatus {
switch tunnel.connectionStatus {
case .active:
return .marked

View File

@ -33,7 +33,7 @@ struct TunnelRestartButton<Label>: View where Label: View {
private var profileProcessor: ProfileProcessor
@ObservedObject
var tunnel: Tunnel
var tunnel: ExtendedTunnel
let profile: Profile?

View File

@ -35,7 +35,7 @@ struct AboutRouterView: View {
let profileManager: ProfileManager
let tunnel: Tunnel
let tunnel: ExtendedTunnel
@State
var navigationRoute: NavigationRoute?
@ -63,7 +63,10 @@ extension AboutRouterView {
DonateView()
case .diagnostics:
DiagnosticsView()
DiagnosticsView(
profileManager: profileManager,
tunnel: tunnel
)
case .appDebugLog(let title):
DebugLogView.withApp(parameters: Constants.shared.log)

View File

@ -35,7 +35,7 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
private let profileManager: ProfileManager
private let tunnel: Tunnel
private let tunnel: ExtendedTunnel
private let registry: Registry
@ -53,7 +53,7 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
public init(
profileManager: ProfileManager,
tunnel: Tunnel,
tunnel: ExtendedTunnel,
registry: Registry
) {
self.profileManager = profileManager

View File

@ -28,12 +28,12 @@ import PassepartoutKit
import SwiftUI
import UtilsLibrary
struct ProfileContainerView: View, Routable, TunnelInstallationProviding {
struct ProfileContainerView: View, Routable {
let layout: ProfilesLayout
let profileManager: ProfileManager
let tunnel: Tunnel
let tunnel: ExtendedTunnel
let registry: Registry

View File

@ -37,7 +37,7 @@ struct ProfileGridView: View, Routable, TunnelInstallationProviding {
var profileManager: ProfileManager
@ObservedObject
var tunnel: Tunnel
var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager

View File

@ -43,7 +43,7 @@ struct ProfileListView: View, Routable, TunnelInstallationProviding {
var profileManager: ProfileManager
@ObservedObject
var tunnel: Tunnel
var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager

View File

@ -36,7 +36,7 @@ struct ProviderEntitySelector: View {
var profileManager: ProfileManager
@ObservedObject
var tunnel: Tunnel
var tunnel: ExtendedTunnel
let profile: Profile

View File

@ -32,19 +32,22 @@ import SwiftUI
public struct AppMenu: View {
@EnvironmentObject
@ObservedObject
private var profileManager: ProfileManager
@EnvironmentObject
@ObservedObject
private var profileProcessor: ProfileProcessor
@EnvironmentObject
private var tunnel: Tunnel
@ObservedObject
private var tunnel: ExtendedTunnel
@StateObject
private var model = Model()
public init() {
public init(profileManager: ProfileManager, profileProcessor: ProfileProcessor, tunnel: ExtendedTunnel) {
self.profileManager = profileManager
self.profileProcessor = profileProcessor
self.tunnel = tunnel
}
public var body: some View {

View File

@ -28,17 +28,17 @@
import PassepartoutKit
import SwiftUI
public struct AppMenuImage: View, TunnelContextProviding {
public struct AppMenuImage: View {
@ObservedObject
public var connectionObserver: ConnectionObserver
private var tunnel: ExtendedTunnel
public init(connectionObserver: ConnectionObserver) {
self.connectionObserver = connectionObserver
public init(tunnel: ExtendedTunnel) {
self.tunnel = tunnel
}
public var body: some View {
ThemeMenuImage(tunnelConnectionStatus.imageName)
ThemeMenuImage(tunnel.connectionStatus.imageName)
}
}

View File

@ -35,7 +35,7 @@ extension DebugLogView {
}
}
static func withTunnel(_ tunnel: Tunnel, parameters: Constants.Log) -> DebugLogView {
static func withTunnel(_ tunnel: ExtendedTunnel, parameters: Constants.Log) -> DebugLogView {
DebugLogView {
await tunnel.currentLog(parameters: parameters)
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import AppLibrary
import CommonLibrary
import PassepartoutKit
import SwiftUI
@ -41,15 +42,16 @@ struct DiagnosticsView: View {
@EnvironmentObject
private var theme: Theme
@EnvironmentObject
private var connectionObserver: ConnectionObserver
@EnvironmentObject
private var iapManager: IAPManager
@AppStorage(AppPreference.logsPrivateData.key, store: .appGroup)
private var logsPrivateData = false
let profileManager: ProfileManager
let tunnel: ExtendedTunnel
var availableTunnelLogs: () async -> [LogEntry] = {
await Task.detached {
PassepartoutConfiguration.shared.availableLogs(at: BundleConfiguration.urlForTunnelLog)
@ -128,7 +130,7 @@ private extension DiagnosticsView {
}
var openVPNSection: some View {
connectionObserver.value(forKey: TunnelEnvironmentKeys.OpenVPN.serverConfiguration)
tunnel.value(forKey: TunnelEnvironmentKeys.OpenVPN.serverConfiguration)
.map { cfg in
Group {
NavigationLink(Strings.Views.Diagnostics.Openvpn.Rows.serverConfiguration) {
@ -143,7 +145,8 @@ private extension DiagnosticsView {
var reportIssueSection: some View {
Section {
ReportIssueButton(
tunnel: connectionObserver.tunnel,
profileManager: profileManager,
tunnel: tunnel,
title: Strings.Views.Diagnostics.ReportIssue.title,
purchasedProducts: iapManager.purchasedProducts,
isUnableToEmail: $isPresentingUnableToEmail
@ -190,7 +193,7 @@ private extension DiagnosticsView {
}
#Preview {
DiagnosticsView {
DiagnosticsView(profileManager: .mock, tunnel: .mock) {
[
.init(date: Date(), url: URL(string: "http://one.com")!),
.init(date: Date().addingTimeInterval(-60), url: URL(string: "http://two.com")!),

View File

@ -29,13 +29,13 @@ import SwiftUI
struct ReportIssueButton {
@EnvironmentObject
private var profileManager: ProfileManager
@EnvironmentObject
private var providerManager: ProviderManager
let tunnel: Tunnel
@ObservedObject
var profileManager: ProfileManager
let tunnel: ExtendedTunnel
let title: String

View File

@ -29,10 +29,10 @@ import SwiftUI
struct ProviderContentModifier<Entity, ProviderRows>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View {
@EnvironmentObject
private var providerManager: ProviderManager
private var iapManager: IAPManager
@EnvironmentObject
private var iapManager: IAPManager
private var providerManager: ProviderManager
let apis: [APIMapper]

View File

@ -32,11 +32,11 @@ import SwiftUI
public struct AppCoordinator: View, AppCoordinatorConforming {
private let profileManager: ProfileManager
private let tunnel: Tunnel
private let tunnel: ExtendedTunnel
private let registry: Registry
public init(profileManager: ProfileManager, tunnel: Tunnel, registry: Registry) {
public init(profileManager: ProfileManager, tunnel: ExtendedTunnel, registry: Registry) {
self.profileManager = profileManager
self.tunnel = tunnel
self.registry = registry

View File

@ -1,5 +1,5 @@
//
// ConnectionObserverTests.swift
// ExtendedTunnelTests.swift
// Passepartout
//
// Created by Davide De Rosa on 9/12/24.
@ -28,15 +28,15 @@ import Foundation
import PassepartoutKit
import XCTest
final class ConnectionObserverTests: XCTestCase {
final class ExtendedTunnelTests: XCTestCase {
}
@MainActor
extension ConnectionObserverTests {
extension ExtendedTunnelTests {
func test_givenTunnel_whenDisconnectWithError_thenPublishesLastErrorCode() async throws {
let env = InMemoryEnvironment()
let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env))
let sut = ConnectionObserver(tunnel: tunnel, environment: env, interval: 0.1)
let sut = ExtendedTunnel(tunnel: tunnel, environment: env, interval: 0.1)
sut.observeObjects()
let profile = try Profile.Builder().tryBuild()
@ -51,7 +51,7 @@ extension ConnectionObserverTests {
func test_givenTunnel_whenConnect_thenPublishesDataCount() async throws {
let env = InMemoryEnvironment()
let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env))
let sut = ConnectionObserver(tunnel: tunnel, environment: env, interval: 0.1)
let sut = ExtendedTunnel(tunnel: tunnel, environment: env, interval: 0.1)
sut.observeObjects()
let profile = try Profile.Builder().tryBuild()