Skip to content

Commit

Permalink
storing custom lists in settings
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii authored and buggmagnet committed Feb 16, 2024
1 parent 757e279 commit e79f029
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
20 changes: 20 additions & 0 deletions ios/MullvadSettings/CustomList.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
103 changes: 103 additions & 0 deletions ios/MullvadSettings/CustomListRepository.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
37 changes: 37 additions & 0 deletions ios/MullvadSettings/CustomListRepositoryProtocol.swift
Original file line number Diff line number Diff line change
@@ -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]
}
1 change: 1 addition & 0 deletions ios/MullvadSettings/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
16 changes: 16 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
79 changes: 79 additions & 0 deletions ios/MullvadVPNTests/CustomListRepositoryTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit e79f029

Please sign in to comment.