summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2024-02-08 18:28:31 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-02-16 10:59:29 +0100
commite79f0295fe08469a864efd759aa68a133cc03f65 (patch)
tree7f03fa3094852a753cbd97f4c1a46b4806c7f301
parent757e279720092d284a6d79b05918b90306b58cd8 (diff)
downloadmullvadvpn-e79f0295fe08469a864efd759aa68a133cc03f65.tar.xz
mullvadvpn-e79f0295fe08469a864efd759aa68a133cc03f65.zip
storing custom lists in settings
-rw-r--r--ios/MullvadSettings/CustomList.swift20
-rw-r--r--ios/MullvadSettings/CustomListRepository.swift103
-rw-r--r--ios/MullvadSettings/CustomListRepositoryProtocol.swift37
-rw-r--r--ios/MullvadSettings/SettingsStore.swift1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj16
-rw-r--r--ios/MullvadVPNTests/CustomListRepositoryTests.swift79
6 files changed, 256 insertions, 0 deletions
diff --git a/ios/MullvadSettings/CustomList.swift b/ios/MullvadSettings/CustomList.swift
new file mode 100644
index 0000000000..51066c7281
--- /dev/null
+++ b/ios/MullvadSettings/CustomList.swift
@@ -0,0 +1,20 @@
+//
+// CustomList.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-01-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+public struct CustomList: Codable, Equatable {
+ public let id: UUID
+ public var name: String
+ public var list: [RelayLocation] = []
+ public init(id: UUID, name: String) {
+ self.id = id
+ self.name = name
+ }
+}
diff --git a/ios/MullvadSettings/CustomListRepository.swift b/ios/MullvadSettings/CustomListRepository.swift
new file mode 100644
index 0000000000..e900ff355c
--- /dev/null
+++ b/ios/MullvadSettings/CustomListRepository.swift
@@ -0,0 +1,103 @@
+//
+// CustomListRepository.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-01-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import Foundation
+import MullvadLogging
+import MullvadTypes
+
+public enum CustomRelayListError: LocalizedError, Equatable {
+ case duplicateName
+
+ public var errorDescription: String? {
+ switch self {
+ case .duplicateName:
+ NSLocalizedString(
+ "DUPLICATE_CUSTOM_LIST_ERROR",
+ tableName: "CustomListRepository",
+ value: "Name is already taken.",
+ comment: ""
+ )
+ }
+ }
+}
+
+public struct CustomListRepository: CustomListRepositoryProtocol {
+ public var publisher: AnyPublisher<[CustomList], Never> {
+ passthroughSubject.eraseToAnyPublisher()
+ }
+
+ private let logger = Logger(label: "CustomListRepository")
+ private let passthroughSubject = PassthroughSubject<[CustomList], Never>()
+
+ private let settingsParser: SettingsParser = {
+ SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
+ }()
+
+ public init() {}
+
+ public func create(_ name: String) throws -> CustomList {
+ var lists = fetchAll()
+ if lists.contains(where: { $0.name == name }) {
+ throw CustomRelayListError.duplicateName
+ } else {
+ let item = CustomList(id: UUID(), name: name)
+ lists.append(item)
+ try write(lists)
+ return item
+ }
+ }
+
+ public func delete(id: UUID) {
+ do {
+ var lists = fetchAll()
+ if let index = lists.firstIndex(where: { $0.id == id }) {
+ lists.remove(at: index)
+ try write(lists)
+ }
+ } catch {
+ logger.error(error: error)
+ }
+ }
+
+ public func fetch(by id: UUID) -> CustomList? {
+ try? read().first(where: { $0.id == id })
+ }
+
+ public func fetchAll() -> [CustomList] {
+ (try? read()) ?? []
+ }
+
+ public func update(_ list: CustomList) {
+ do {
+ var lists = fetchAll()
+ if let index = lists.firstIndex(where: { $0.id == list.id }) {
+ lists[index] = list
+ try write(lists)
+ }
+ } catch {
+ logger.error(error: error)
+ }
+ }
+}
+
+extension CustomListRepository {
+ private func read() throws -> [CustomList] {
+ let data = try SettingsManager.store.read(key: .customRelayLists)
+
+ return try settingsParser.parseUnversionedPayload(as: [CustomList].self, from: data)
+ }
+
+ private func write(_ list: [CustomList]) throws {
+ let data = try settingsParser.produceUnversionedPayload(list)
+
+ try SettingsManager.store.write(data, for: .customRelayLists)
+
+ passthroughSubject.send(list)
+ }
+}
diff --git a/ios/MullvadSettings/CustomListRepositoryProtocol.swift b/ios/MullvadSettings/CustomListRepositoryProtocol.swift
new file mode 100644
index 0000000000..42c498d452
--- /dev/null
+++ b/ios/MullvadSettings/CustomListRepositoryProtocol.swift
@@ -0,0 +1,37 @@
+//
+// CustomListRepositoryProtocol.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-01-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import Foundation
+import MullvadTypes
+public protocol CustomListRepositoryProtocol {
+ /// Publisher that propagates a snapshot of persistent store upon modifications.
+ var publisher: AnyPublisher<[CustomList], Never> { get }
+
+ /// Persist modified custom list locating existing entry by id.
+ /// - Parameter list: persistent custom list model.
+ func update(_ list: CustomList)
+
+ /// Delete custom list by id.
+ /// - Parameter id: an access method id.
+ func delete(id: UUID)
+
+ /// Fetch custom list by id.
+ /// - Parameter id: a custom list id.
+ /// - Returns: a persistent custom list model upon success, otherwise `nil`.
+ func fetch(by id: UUID) -> CustomList?
+
+ /// Create a custom list by unique name.
+ /// - Parameter name: a custom list name.
+ /// - Returns: a persistent custom list model upon success, otherwise throws `Error`.
+ func create(_ name: String) throws -> CustomList
+
+ /// Fetch all custom list.
+ /// - Returns: all custom list model .
+ func fetchAll() -> [CustomList]
+}
diff --git a/ios/MullvadSettings/SettingsStore.swift b/ios/MullvadSettings/SettingsStore.swift
index 0b4c98dbb6..2901ae2bb3 100644
--- a/ios/MullvadSettings/SettingsStore.swift
+++ b/ios/MullvadSettings/SettingsStore.swift
@@ -13,6 +13,7 @@ public enum SettingsKey: String, CaseIterable {
case deviceState = "DeviceState"
case apiAccessMethods = "ApiAccessMethods"
case ipOverrides = "IPOverrides"
+ case customRelayLists = "CustomRelayLists"
case lastUsedAccount = "LastUsedAccount"
case shouldWipeSettings = "ShouldWipeSettings"
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index efda36cf79..17a4b9ae7f 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -785,6 +785,10 @@
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
F04F95A12B21D24400431E08 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = F04F95A02B21D24400431E08 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; };
F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; };
+ F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */; };
+ F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE562B7376C6003F4EDB /* CustomListRepository.swift */; };
+ F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE592B7376F4003F4EDB /* CustomList.swift */; };
+ F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */; };
F05F39942B21C6C6006E60A7 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
@@ -1890,6 +1894,10 @@
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
F04F95A02B21D24400431E08 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = shadowsocks.h; sourceTree = "<group>"; };
F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
+ F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryProtocol.swift; sourceTree = "<group>"; };
+ F050AE562B7376C6003F4EDB /* CustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepository.swift; sourceTree = "<group>"; };
+ F050AE592B7376F4003F4EDB /* CustomList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = "<group>"; };
+ F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryTests.swift; sourceTree = "<group>"; };
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
@@ -2809,6 +2817,7 @@
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */,
A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
+ F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */,
58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */,
A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */,
58FBFBF0291630700020E046 /* DurationTests.swift */,
@@ -2878,6 +2887,9 @@
5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */,
58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */,
F0164EBB2B482E430020268D /* AppStorage.swift */,
+ F050AE592B7376F4003F4EDB /* CustomList.swift */,
+ F050AE562B7376C6003F4EDB /* CustomListRepository.swift */,
+ F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */,
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
580F8B8528197958002E0998 /* DNSSettings.swift */,
7A5869B22B5697AC00640D27 /* IPOverride.swift */,
@@ -4678,6 +4690,7 @@
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */,
F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */,
+ F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */,
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */,
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */,
A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */,
@@ -4773,8 +4786,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */,
7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */,
58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */,
+ F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */,
58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */,
A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */,
F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */,
@@ -4798,6 +4813,7 @@
F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */,
58B2FDE92AA71D5C003EB5C6 /* SettingsParser.swift in Sources */,
F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */,
+ F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */,
58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */,
F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */,
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */,
diff --git a/ios/MullvadVPNTests/CustomListRepositoryTests.swift b/ios/MullvadVPNTests/CustomListRepositoryTests.swift
new file mode 100644
index 0000000000..d7b80a6374
--- /dev/null
+++ b/ios/MullvadVPNTests/CustomListRepositoryTests.swift
@@ -0,0 +1,79 @@
+//
+// CustomListRepositoryTests.swift
+// MullvadVPNTests
+//
+// Created by Mojgan on 2024-02-07.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadSettings
+import Network
+import XCTest
+
+class CustomListRepositoryTests: XCTestCase {
+ static let store = InMemorySettingsStore<SettingNotFound>()
+ private var repository = CustomListRepository()
+
+ override class func setUp() {
+ SettingsManager.unitTestStore = store
+ }
+
+ override class func tearDown() {
+ SettingsManager.unitTestStore = nil
+ }
+
+ override func tearDownWithError() throws {
+ repository.fetchAll().forEach {
+ repository.delete(id: $0.id)
+ }
+ }
+
+ func testFailedAddingDuplicateCustomList() throws {
+ let name = "Netflix"
+ let item = try XCTUnwrap(repository.create(name))
+ XCTAssertThrowsError(try repository.create(item.name)) { error in
+ XCTAssertEqual(error as? CustomRelayListError, CustomRelayListError.duplicateName)
+ }
+ }
+
+ func testAddingCustomList() throws {
+ let name = "Netflix"
+
+ var item = try XCTUnwrap(repository.create(name))
+ item.list.append(.country("SE"))
+ item.list.append(.city("SE", "Gothenburg"))
+
+ repository.update(item)
+
+ let storedItem = repository.fetch(by: item.id)
+ XCTAssertEqual(storedItem, item)
+ }
+
+ func testDeletingCustomList() throws {
+ let name = "Netflix"
+
+ var item = try XCTUnwrap(repository.create(name))
+ item.list.append(.country("SE"))
+ item.list.append(.city("SE", "Gothenburg"))
+ repository.update(item)
+
+ let storedItem = repository.fetch(by: item.id)
+ repository.delete(id: try XCTUnwrap(storedItem?.id))
+
+ XCTAssertNil(repository.fetch(by: item.id))
+ }
+
+ func testFetchingAllCustomList() throws {
+ var streaming = try XCTUnwrap(repository.create("Netflix"))
+ streaming.list.append(.country("FR"))
+ streaming.list.append(.city("SE", "Gothenburg"))
+ repository.update(streaming)
+
+ var gaming = try XCTUnwrap(repository.create("PS5"))
+ gaming.list.append(.country("DE"))
+ gaming.list.append(.city("SE", "Gothenburg"))
+ repository.update(streaming)
+
+ XCTAssertEqual(repository.fetchAll().count, 2)
+ }
+}