//
// ProfileEditorTests.swift
// Passepartout
//
// Created by Davide De Rosa on 9/12/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 .
//
import Combine
import CommonLibrary
import Foundation
import PassepartoutKit
@testable import UILibrary
import XCTest
final class ProfileEditorTests: XCTestCase {
private var subscriptions: Set = []
}
@MainActor
extension ProfileEditorTests {
// MARK: CRUD
func test_givenModules_thenMatchesModules() {
let sut = ProfileEditor(modules: [
DNSModule.Builder(),
IPModule.Builder()
])
XCTAssertTrue(sut.profile.name.isEmpty)
XCTAssertTrue(sut.modules[0] is DNSModule.Builder)
XCTAssertTrue(sut.modules[1] is IPModule.Builder)
}
func test_givenProfile_thenMatchesProfile() throws {
let name = "foobar"
let dns = try DNSModule.Builder().tryBuild()
let ip = IPModule.Builder().tryBuild()
let profile = try Profile.Builder(
name: name,
modules: [dns, ip],
activeModulesIds: [dns.id]
).tryBuild()
let sut = ProfileEditor(profile: profile)
XCTAssertEqual(sut.profile.name, name)
XCTAssertTrue(sut.modules[0] is DNSModule.Builder)
XCTAssertTrue(sut.modules[1] is IPModule.Builder)
XCTAssertEqual(sut.activeModulesIds, [dns.id])
}
func test_givenProfileWithModules_thenExcludesModuleTypes() {
let sut = ProfileEditor(modules: [
DNSModule.Builder(),
IPModule.Builder()
])
let moduleTypes = sut.availableModuleTypes
XCTAssertFalse(moduleTypes.contains(.dns))
XCTAssertTrue(moduleTypes.contains(.httpProxy))
XCTAssertFalse(moduleTypes.contains(.ip))
XCTAssertTrue(moduleTypes.contains(.onDemand))
// provider only
XCTAssertTrue(moduleTypes.contains(.openVPN))
// until editable
XCTAssertFalse(moduleTypes.contains(.wireGuard))
}
func test_givenModules_thenReturnsModuleById() {
let dns = DNSModule.Builder()
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [dns, ip])
XCTAssertEqual(sut.modules[0].id, dns.id)
XCTAssertEqual(sut.modules[1].id, ip.id)
XCTAssertTrue(sut.module(withId: dns.id) is DNSModule.Builder)
XCTAssertTrue(sut.module(withId: ip.id) is IPModule.Builder)
XCTAssertNil(sut.module(withId: UUID()))
}
func test_givenModules_whenMove_thenMovesModules() {
let dns = DNSModule.Builder()
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [dns, ip])
sut.moveModules(from: IndexSet(integer: 0), to: 2)
XCTAssertEqual(sut.modules[0].id, ip.id)
XCTAssertEqual(sut.modules[1].id, dns.id)
}
func test_givenModules_whenRemove_thenRemovesModules() {
let dns = DNSModule.Builder()
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [dns, ip])
sut.removeModules(at: IndexSet(integer: 0))
XCTAssertEqual(sut.modules.count, 1)
XCTAssertEqual(sut.modules[0].id, ip.id)
XCTAssertEqual(Set(sut.removedModules.keys), [dns.id])
sut.removeModule(withId: dns.id)
sut.removeModule(withId: ip.id)
XCTAssertTrue(sut.modules.isEmpty)
XCTAssertEqual(Set(sut.removedModules.keys), [dns.id, ip.id])
}
func test_givenModules_whenSaveNew_thenAppendsNew() {
let dns = DNSModule.Builder()
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [dns, ip])
sut.saveModule(ip, activating: false)
XCTAssertTrue(sut.modules[1] is IPModule.Builder)
XCTAssertEqual(sut.activeModulesIds, [dns.id, ip.id])
}
func test_givenModules_whenSaveExisting_thenReplacesExisting() throws {
var dns = DNSModule.Builder()
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [dns, ip])
dns.protocolType = .tls
sut.saveModule(dns, activating: false)
XCTAssertEqual(sut.activeModulesIds, [dns.id, ip.id])
let newDNS = try XCTUnwrap(sut.modules[0] as? DNSModule.Builder)
XCTAssertEqual(newDNS.protocolType, dns.protocolType)
}
func test_givenModules_whenSaveActivating_thenActivates() {
let dns = DNSModule.Builder()
let sut = ProfileEditor(modules: [])
sut.saveModule(dns, activating: true)
XCTAssertEqual(sut.activeModulesIds, [dns.id])
}
// MARK: - Active modules
func test_givenModules_whenToggle_thenToggles() throws {
let dns = DNSModule.Builder()
let proxy = HTTPProxyModule.Builder()
let sut = ProfileEditor(modules: [dns, proxy])
XCTAssertEqual(sut.activeModulesIds, [dns.id, proxy.id])
sut.toggleModule(withId: dns.id)
XCTAssertEqual(sut.activeModulesIds, [proxy.id])
sut.toggleModule(withId: dns.id)
XCTAssertEqual(sut.activeModulesIds, [dns.id, proxy.id])
sut.toggleModule(withId: dns.id)
sut.toggleModule(withId: proxy.id)
XCTAssertEqual(sut.activeModulesIds, [])
}
func test_givenModules_whenMultipleConnections_thenFailsToBuild() throws {
let ovpn = OpenVPNModule.Builder()
let wg = WireGuardModule.Builder(configurationBuilder: .default)
let sut = ProfileEditor(modules: [ovpn, wg])
XCTAssertEqual(sut.activeModulesIds, [ovpn.id, wg.id])
sut.toggleModule(withId: wg.id)
XCTAssertEqual(sut.activeModulesIds, [ovpn.id])
XCTAssertThrowsError(try sut.build())
}
func test_givenModulesWithoutConnection_whenActiveIP_thenFailsToBuild() throws {
let ip = IPModule.Builder()
let sut = ProfileEditor(modules: [ip])
XCTAssertEqual(sut.activeModulesIds, [ip.id])
XCTAssertThrowsError(try sut.build())
}
// MARK: Building
func test_givenProfile_whenBuild_thenSucceeds() throws {
let wg = WireGuardModule.Builder(configurationBuilder: .default)
let sut = ProfileEditor(modules: [wg])
sut.profile.name = "hello"
let profile = try sut.build()
XCTAssertEqual(profile.name, "hello")
XCTAssertTrue(profile.modules.first is WireGuardModule)
XCTAssertEqual(profile.modules.first as? WireGuardModule, try wg.tryBuild())
XCTAssertEqual(profile.activeModulesIds, [wg.id])
}
func test_givenProfile_whenBuildWithEmptyName_thenFails() async throws {
let sut = ProfileEditor(modules: [])
XCTAssertThrowsError(try sut.build())
}
func test_givenProfile_whenBuildWithMalformedModule_thenFails() async throws {
let dns = DNSModule.Builder(protocolType: .https) // missing URL
let sut = ProfileEditor(modules: [dns])
XCTAssertThrowsError(try sut.build())
}
// MARK: Saving
func test_givenProfileManager_whenSave_thenSavesProfileToManager() async throws {
let name = "foobar"
let dns = try DNSModule.Builder().tryBuild()
let ip = IPModule.Builder().tryBuild()
let profile = try Profile.Builder(
name: name,
modules: [dns, ip],
activeModulesIds: [dns.id]
).tryBuild()
let sut = ProfileEditor(profile: profile)
let manager = ProfileManager(profiles: [])
let exp = expectation(description: "Save")
manager
.didChange
.receive(on: ImmediateScheduler.shared)
.sink {
switch $0 {
case .save(let savedProfile):
do {
let lhs = try savedProfile.withoutUserInfo()
let rhs = try profile.withoutUserInfo()
XCTAssertEqual(lhs, rhs)
} catch {
XCTFail(error.localizedDescription)
}
exp.fulfill()
default:
break
}
}
.store(in: &subscriptions)
try await sut.save(to: manager)
await fulfillment(of: [exp])
}
}
private extension WireGuard.Configuration.Builder {
static var `default`: WireGuard.Configuration.Builder {
WireGuard.Configuration.Builder(privateKey: "")
}
}