Skip to content

Commit

Permalink
Merge branch 'currently-in-use-api-access-method-should-be-visible-in…
Browse files Browse the repository at this point in the history
…-ui-ios-470'
  • Loading branch information
buggmagnet committed Jan 30, 2024
2 parents 2141af9 + 851803b commit 21d1883
Show file tree
Hide file tree
Showing 22 changed files with 315 additions and 153 deletions.
17 changes: 6 additions & 11 deletions ios/MullvadREST/Transport/AccessMethodIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Foundation
import MullvadSettings

class AccessMethodIterator {
private var lastReachableApiAccessCache: LastReachableApiAccessCache
private let dataSource: AccessMethodRepositoryDataSource

private var index = 0
Expand All @@ -21,19 +20,15 @@ class AccessMethodIterator {
dataSource.fetchAll().filter { $0.isEnabled }
}

private var lastReachableApiAccessId: UUID {
lastReachableApiAccessCache.id
private var lastReachableApiAccessId: UUID? {
dataSource.fetchLastReachable().id
}

init(_ userDefaults: UserDefaults, dataSource: AccessMethodRepositoryDataSource) {
init(dataSource: AccessMethodRepositoryDataSource) {
self.dataSource = dataSource
self.lastReachableApiAccessCache = LastReachableApiAccessCache(
defaultValue: dataSource.directAccess.id,
container: userDefaults
)

self.dataSource
.publisher
.accessMethodsPublisher
.sink { [weak self] _ in
guard let self else { return }
self.refreshCacheIfNeeded()
Expand All @@ -48,14 +43,14 @@ class AccessMethodIterator {
} else {
/// When `firstIndex` is `nil`, that means the current configuration is not valid anymore
/// Invalidating cache by replacing the `current` to the next enabled access method
lastReachableApiAccessCache.id = pick().id
dataSource.saveLastReachable(pick())
}
}

func rotate() {
let (partial, isOverflow) = index.addingReportingOverflow(1)
index = isOverflow ? 0 : partial
lastReachableApiAccessCache.id = pick().id
dataSource.saveLastReachable(pick())
}

func pick() -> PersistentAccessMethod {
Expand Down
33 changes: 0 additions & 33 deletions ios/MullvadREST/Transport/LastReachableApiAccessCache.swift

This file was deleted.

8 changes: 2 additions & 6 deletions ios/MullvadREST/Transport/TransportStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Logging
import MullvadSettings
import MullvadTypes

public class TransportStrategy: Equatable {
public struct TransportStrategy: Equatable {
/// The different transports suggested by the strategy
public enum Transport: Equatable {
/// Connecting a direct connection
Expand Down Expand Up @@ -45,15 +45,11 @@ public class TransportStrategy: Equatable {
private let accessMethodIterator: AccessMethodIterator

public init(
_ userDefaults: UserDefaults,
datasource: AccessMethodRepositoryDataSource,
shadowsocksLoader: ShadowsocksLoaderProtocol
) {
self.shadowsocksLoader = shadowsocksLoader
self.accessMethodIterator = AccessMethodIterator(
userDefaults,
dataSource: datasource
)
self.accessMethodIterator = AccessMethodIterator(dataSource: datasource)
}

/// Rotating between enabled configurations by what order they were added in
Expand Down
14 changes: 10 additions & 4 deletions ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import Combine
import MullvadSettings

typealias PersistentAccessMethod = MullvadSettings.PersistentAccessMethod
struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource {
var directAccess: MullvadSettings.PersistentAccessMethod
var publisher: AnyPublisher<[MullvadSettings.PersistentAccessMethod], Never> {
var directAccess: PersistentAccessMethod

var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> {
passthroughSubject.eraseToAnyPublisher()
}

Expand All @@ -23,7 +23,13 @@ struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource {
passthroughSubject.send(accessMethods)
}

func fetchAll() -> [MullvadSettings.PersistentAccessMethod] {
func fetchAll() -> [PersistentAccessMethod] {
passthroughSubject.value
}

func saveLastReachable(_ method: PersistentAccessMethod) {}

func fetchLastReachable() -> PersistentAccessMethod {
directAccess
}
}
21 changes: 0 additions & 21 deletions ios/MullvadRESTTests/TransportStrategyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,13 @@
import XCTest

class TransportStrategyTests: XCTestCase {
var userDefaults: UserDefaults!
static var suiteName: String!

private var directAccess: PersistentAccessMethod!
private var bridgeAccess: PersistentAccessMethod!

private var shadowsocksLoader: ShadowsocksLoaderStub!

override class func setUp() {
super.setUp()
suiteName = UUID().uuidString
}

override func setUpWithError() throws {
try super.setUpWithError()
userDefaults = UserDefaults(suiteName: Self.suiteName)

shadowsocksLoader = ShadowsocksLoaderStub(configuration: ShadowsocksConfiguration(
address: .ipv4(.loopback),
Expand All @@ -51,16 +42,10 @@ class TransportStrategyTests: XCTestCase {
)
}

override func tearDownWithError() throws {
userDefaults.removePersistentDomain(forName: Self.suiteName)
try super.tearDownWithError()
}

func testDefaultStrategyIsDirectWhenAllMethodsAreDisabled() throws {
directAccess.isEnabled = false
bridgeAccess.isEnabled = false
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand All @@ -76,7 +61,6 @@ class TransportStrategyTests: XCTestCase {
func testReuseSameStrategyWhenEverythingElseIsDisabled() throws {
directAccess.isEnabled = false
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand All @@ -96,7 +80,6 @@ class TransportStrategyTests: XCTestCase {

func testLoopsFromTheStartAfterTryingAllEnabledStrategies() {
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand Down Expand Up @@ -130,7 +113,6 @@ class TransportStrategyTests: XCTestCase {
func testUsesNextWhenItIsNotReachable() {
bridgeAccess.isEnabled = false
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand Down Expand Up @@ -164,7 +146,6 @@ class TransportStrategyTests: XCTestCase {
func testGoToNextStrategyWhenItFailsToLoadBridgeConfiguration() {
shadowsocksLoader.error = IOError.fileNotFound
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand All @@ -180,7 +161,6 @@ class TransportStrategyTests: XCTestCase {
shadowsocksLoader.error = IOError.fileNotFound
directAccess.isEnabled = false
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand All @@ -207,7 +187,6 @@ class TransportStrategyTests: XCTestCase {
authentication: authentication
)
let transportStrategy = TransportStrategy(
userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
Expand Down
86 changes: 59 additions & 27 deletions ios/MullvadSettings/AccessMethodRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,72 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
proxyConfiguration: .bridges
)

let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([])
private let accessMethodsSubject: CurrentValueSubject<[PersistentAccessMethod], Never>
public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> {
accessMethodsSubject.eraseToAnyPublisher()
}

public var publisher: AnyPublisher<[PersistentAccessMethod], Never> {
passthroughSubject.eraseToAnyPublisher()
private let lastReachableAccessMethodSubject: CurrentValueSubject<PersistentAccessMethod, Never>
public var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> {
lastReachableAccessMethodSubject.eraseToAnyPublisher()
}

public var directAccess: PersistentAccessMethod {
direct
}

public init() {
accessMethodsSubject = CurrentValueSubject([])
lastReachableAccessMethodSubject = CurrentValueSubject(direct)

add([direct, bridge])

accessMethodsSubject.send(fetchAll())
lastReachableAccessMethodSubject.send(fetchLastReachable())
}

public func save(_ method: PersistentAccessMethod) {
var storedMethods = fetchAll()
var methodStore = readApiAccessMethodStore()

if let index = storedMethods.firstIndex(where: { $0.id == method.id }) {
storedMethods[index] = method
if let index = methodStore.accessMethods.firstIndex(where: { $0.id == method.id }) {
methodStore.accessMethods[index] = method
} else {
storedMethods.append(method)
methodStore.accessMethods.append(method)
}

do {
try writeApiAccessMethods(storedMethods)
try writeApiAccessMethodStore(methodStore)
accessMethodsSubject.send(methodStore.accessMethods)
} catch {
logger.error("Could not update access methods: \(storedMethods) \nError: \(error)")
logger.error("Could not save access method: \(method) \nError: \(error)")
}
}

public func saveLastReachable(_ method: PersistentAccessMethod) {
var methodStore = readApiAccessMethodStore()
methodStore.lastReachableAccessMethod = method

do {
try writeApiAccessMethodStore(methodStore)
lastReachableAccessMethodSubject.send(method)
} catch {
logger.error("Could not save last reachable access method: \(method) \nError: \(error)")
}
}

public func delete(id: UUID) {
var methods = fetchAll()
guard let index = methods.firstIndex(where: { $0.id == id }) else { return }
var methodStore = readApiAccessMethodStore()
guard let index = methodStore.accessMethods.firstIndex(where: { $0.id == id }) else { return }

// Prevent removing methods that have static UUIDs and are always present.
let method = methods[index]
let method = methodStore.accessMethods[index]
if !method.kind.isPermanent {
methods.remove(at: index)
methodStore.accessMethods.remove(at: index)
}

do {
try writeApiAccessMethods(methods)
try writeApiAccessMethodStore(methodStore)
accessMethodsSubject.send(methodStore.accessMethods)
} catch {
logger.error("Could not delete access method with id: \(id) \nError: \(error)")
}
Expand All @@ -79,43 +103,51 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
}

public func fetchAll() -> [PersistentAccessMethod] {
(try? readApiAccessMethods()) ?? []
readApiAccessMethodStore().accessMethods
}

public func fetchLastReachable() -> PersistentAccessMethod {
readApiAccessMethodStore().lastReachableAccessMethod
}

public func reloadWithDefaultsAfterDataRemoval() {
add([direct, bridge])
}

private func add(_ methods: [PersistentAccessMethod]) {
var storedMethods = fetchAll()
var methodStore = readApiAccessMethodStore()

methods.forEach { method in
if !storedMethods.contains(where: { $0.id == method.id }) {
storedMethods.append(method)
if !methodStore.accessMethods.contains(where: { $0.id == method.id }) {
methodStore.accessMethods.append(method)
}
}

do {
try writeApiAccessMethods(storedMethods)
try writeApiAccessMethodStore(methodStore)
accessMethodsSubject.send(methods)
} catch {
logger.error("Could not update access methods: \(storedMethods) \nError: \(error)")
logger.error("Could not update access methods: \(methods) \nError: \(error)")
}
}

private func readApiAccessMethods() throws -> [PersistentAccessMethod] {
private func readApiAccessMethodStore() -> PersistentAccessMethodStore {
let parser = makeParser()
let data = try SettingsManager.store.read(key: .apiAccessMethods)

return try parser.parseUnversionedPayload(as: [PersistentAccessMethod].self, from: data)
do {
let data = try SettingsManager.store.read(key: .apiAccessMethods)
return try parser.parseUnversionedPayload(as: PersistentAccessMethodStore.self, from: data)
} catch {
logger.error("Could not load access method store: \(error)")
return PersistentAccessMethodStore(lastReachableAccessMethod: direct, accessMethods: [])
}
}

private func writeApiAccessMethods(_ accessMethods: [PersistentAccessMethod]) throws {
private func writeApiAccessMethodStore(_ store: PersistentAccessMethodStore) throws {
let parser = makeParser()
let data = try parser.produceUnversionedPayload(accessMethods)
let data = try parser.produceUnversionedPayload(store)

try SettingsManager.store.write(data, for: .apiAccessMethods)

passthroughSubject.send(accessMethods)
}

private func makeParser() -> SettingsParser {
Expand Down
Loading

0 comments on commit 21d1883

Please sign in to comment.