-
Notifications
You must be signed in to change notification settings - Fork 339
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |