Fixing the broken unit tests, resolved the remaining TODOs

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift
#	SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift
#	SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift
#	SessionMessagingKitTests/_TestUtilities/TestOnionRequestAPI.swift
#	SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift
#	SessionUtilitiesKit/Networking/BatchResponse.swift
#	SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift
This commit is contained in:
Morgan Pretty 2022-12-06 10:13:04 +11:00
parent 5033738994
commit ca4ce52402
22 changed files with 698 additions and 509 deletions

View File

@ -809,6 +809,7 @@
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; };
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; };
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */; };
FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */; };
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */; };
@ -1919,6 +1920,7 @@
FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = "<group>"; };
FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = "<group>"; };
FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAttachmentDownloadsJob.swift; sourceTree = "<group>"; };
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Utilities.swift"; sourceTree = "<group>"; };
@ -3968,7 +3970,7 @@
children = (
FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */,
FD8ECF832934507500C0D1BB /* Networking */,
FD9B30F1293EA0AF008DEE3E /* Networking */,
);
path = SessionUtilitiesKitTests;
sourceTree = "<group>";
@ -4065,6 +4067,14 @@
path = JobRunner;
sourceTree = "<group>";
};
FD9B30F1293EA0AF008DEE3E /* Networking */ = {
isa = PBXGroup;
children = (
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */,
);
path = Networking;
sourceTree = "<group>";
};
FDC2909227D710A9005DAE71 /* Types */ = {
isa = PBXGroup;
children = (
@ -5916,6 +5926,7 @@
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */,
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */,
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,

View File

@ -53,6 +53,7 @@ public enum PushRegistrationError: Error {
return registerUserNotificationSettings()
.setFailureType(to: Error.self)
.receive(on: DispatchQueue.main) // MUST be on main thread
.flatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in
#if targetEnvironment(simulator)
return Fail(error: PushRegistrationError.pushNotSupported(description: "Push not supported on simulators"))

View File

@ -188,7 +188,6 @@ extension SyncPushTokensJob {
}
.sinkUntilComplete(
receiveCompletion: { result in
// TODO: Test these are called correctly
switch result {
case .finished: break
case .failure(let error): failure(error)

View File

@ -26,8 +26,7 @@ public protocol OGMCacheType {
// MARK: - OpenGroupManager
@objc(SNOpenGroupManager)
public final class OpenGroupManager: NSObject {
public final class OpenGroupManager {
// MARK: - Cache
public class Cache: OGMCacheType {
@ -61,7 +60,7 @@ public final class OpenGroupManager: NSObject {
// MARK: - Variables
@objc public static let shared: OpenGroupManager = OpenGroupManager()
public static let shared: OpenGroupManager = OpenGroupManager()
/// Note: This should not be accessed directly but rather via the 'OGMDependencies' type
fileprivate let mutableCache: Atomic<OGMCacheType> = Atomic(Cache())

View File

@ -1,7 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Combine
import SessionSnodeKit
import SessionUtilitiesKit
@ -19,97 +19,117 @@ class BatchRequestInfoSpec: QuickSpec {
// MARK: - Spec
override func spec() {
// MARK: - BatchSubRequest
// MARK: - BatchRequest.Child
describe("a BatchRequest.Child") {
var subRequest: OpenGroupAPI.BatchRequest.Child!
var request: OpenGroupAPI.BatchRequest!
context("when initializing") {
it("sets the headers to nil if there aren't any") {
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
server: "testServer",
endpoint: .batch
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
server: "testServer",
endpoint: .batch
)
)
]
)
expect(subRequest.headers).to(beNil())
expect(request.requests.first?.headers).to(beNil())
}
it("converts the headers to HTTP headers") {
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [.authorization: "testAuth"],
body: nil
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [.authorization: "testAuth"],
body: nil
)
)
]
)
expect(subRequest.headers).to(equal(["Authorization": "testAuth"]))
expect(request.requests.first?.headers).to(equal(["Authorization": "testAuth"]))
}
}
context("when encoding") {
it("successfully encodes a string body") {
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<String, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: "testBody"
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: Request<String, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: "testBody"
)
)
]
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
let childRequestData: Data = try! JSONEncoder().encode(request.requests[0])
let childRequestString: String? = String(data: childRequestData, encoding: .utf8)
expect(subRequestString)
expect(childRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"b64\":\"testBody\"}"))
}
it("successfully encodes a byte body") {
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: [1, 2, 3]
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: [1, 2, 3]
)
)
]
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
let childRequestData: Data = try! JSONEncoder().encode(request.requests[0])
let childRequestString: String? = String(data: childRequestData, encoding: .utf8)
expect(subRequestString)
expect(childRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"bytes\":[1,2,3]}"))
}
it("successfully encodes a JSON body") {
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<TestType, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: TestType(stringValue: "testValue")
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: Request<TestType, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: TestType(stringValue: "testValue")
)
)
]
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
let childRequestData: Data = try! JSONEncoder().encode(request.requests[0])
let childRequestString: String? = String(data: childRequestData, encoding: .utf8)
expect(subRequestString)
expect(childRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"json\":{\"stringValue\":\"testValue\"}}"))
}
}
}
// MARK: - BatchRequestInfo<T, R>
// MARK: - BatchRequest.Info
describe("a BatchRequest.Info") {
var request: Request<TestType, OpenGroupAPI.Endpoint>!
@ -143,27 +163,17 @@ class BatchRequestInfoSpec: QuickSpec {
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
expect(requestInfo.responseType == HTTP.BatchSubResponse<TestType>.self).to(beTrue())
}
it("exposes the endpoint correctly") {
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
request: request
)
}
// MARK: - Convenience
// MARK: --Decodable
describe("a Decodable") {
it("decodes correctly") {
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
let result: TestType? = try? TestType.decoded(from: jsonData)
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
}
it("generates a sub request correctly") {
let batchRequest: OpenGroupAPI.BatchRequest = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: request
)
]
)
expect(batchRequest.requests[0].method).to(equal(request.method))
expect(batchRequest.requests[0].path).to(equal(request.urlPathAndParamsString))
expect(batchRequest.requests[0].headers).to(beNil())
expect(result).to(equal(TestType(stringValue: "testValue")))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import PromiseKit
import Foundation
import Combine
import GRDB
import Sodium
import SessionSnodeKit
@ -779,7 +780,7 @@ class OpenGroupManagerSpec: QuickSpec {
var didComplete: Bool = false // Prevent multi-threading test bugs
mockStorage
.writeAsync { db in
.writePublisherFlatMap { db in
openGroupManager
.add(
db,
@ -790,8 +791,9 @@ class OpenGroupManagerSpec: QuickSpec {
dependencies: dependencies
)
}
.map { _ -> Void in didComplete = true }
.retainUntilComplete()
.subscribe(on: DispatchQueue.main)
.receiveOnMain(immediately: true)
.sinkUntilComplete(receiveCompletion: { _ in didComplete = true })
expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
expect(
@ -810,7 +812,7 @@ class OpenGroupManagerSpec: QuickSpec {
var didComplete: Bool = false // Prevent multi-threading test bugs
mockStorage
.writeAsync { db in
.writePublisherFlatMap { db in
openGroupManager
.add(
db,
@ -821,8 +823,9 @@ class OpenGroupManagerSpec: QuickSpec {
dependencies: dependencies
)
}
.map { _ -> Void in didComplete = true }
.retainUntilComplete()
.subscribe(on: DispatchQueue.main)
.receiveOnMain(immediately: true)
.sinkUntilComplete(receiveCompletion: { _ in didComplete = true })
expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
expect(mockOGMCache)
@ -847,7 +850,7 @@ class OpenGroupManagerSpec: QuickSpec {
var didComplete: Bool = false // Prevent multi-threading test bugs
mockStorage
.writeAsync { db in
.writePublisherFlatMap { db in
openGroupManager
.add(
db,
@ -860,8 +863,9 @@ class OpenGroupManagerSpec: QuickSpec {
dependencies: dependencies
)
}
.map { _ -> Void in didComplete = true }
.retainUntilComplete()
.subscribe(on: DispatchQueue.main)
.receiveOnMain(immediately: true)
.sinkUntilComplete(receiveCompletion: { _ in didComplete = true })
expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
expect(
@ -901,7 +905,7 @@ class OpenGroupManagerSpec: QuickSpec {
var error: Error?
mockStorage
.writeAsync { db in
.writePublisherFlatMap { db in
openGroupManager
.add(
db,
@ -912,8 +916,10 @@ class OpenGroupManagerSpec: QuickSpec {
dependencies: dependencies
)
}
.catch { error = $0 }
.retainUntilComplete()
.subscribe(on: DispatchQueue.main)
.receiveOnMain(immediately: true)
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(error?.localizedDescription)
.toEventually(
@ -1242,8 +1248,10 @@ class OpenGroupManagerSpec: QuickSpec {
).insert(db)
}
mockOGMCache.when { $0.groupImagePromises }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise.value(Data())])
mockOGMCache.when { $0.groupImagePublishers }
.thenReturn([
OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(Data()).setFailureType(to: Error.self).eraseToAnyPublisher()
])
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
@ -1679,8 +1687,10 @@ class OpenGroupManagerSpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.imageData.set(to: nil))
}
mockOGMCache.when { $0.groupImagePromises }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise.value(imageData)])
mockOGMCache.when { $0.groupImagePublishers }
.thenReturn([
OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher()
])
}
it("uses the provided room image id if available") {
@ -1952,8 +1962,10 @@ class OpenGroupManagerSpec: QuickSpec {
it("does nothing if it fails to retrieve the room image") {
var didComplete: Bool = false // Prevent multi-threading test bugs
mockOGMCache.when { $0.groupImagePromises }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise(error: HTTPError.generic)])
mockOGMCache.when { $0.groupImagePublishers }
.thenReturn([
OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: HTTPError.generic).eraseToAnyPublisher()
])
testPollInfo = OpenGroupAPI.RoomPollInfo(
token: "testRoom",
@ -3248,11 +3260,11 @@ class OpenGroupManagerSpec: QuickSpec {
}
it("returns the cached promise if there is one") {
let (promise, _) = Promise<[OpenGroupAPI.Room]>.pending()
mockOGMCache.when { $0.defaultRoomsPromise }.thenReturn(promise)
let publisher = Future { _ in }.eraseToAnyPublisher()
mockOGMCache.when { $0.defaultRoomsPublisher }.thenReturn(publisher)
expect(OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies))
.to(equal(promise))
.to(equal(publisher))
}
it("stores the open group information") {
@ -3494,12 +3506,12 @@ class OpenGroupManagerSpec: QuickSpec {
}
it("retrieves the image retrieval promise from the cache if it exists") {
let (promise, _) = Promise<Data>.pending()
let publisher = Future<Data, Error> { _ in }.eraseToAnyPublisher()
mockOGMCache
.when { $0.groupImagePromises }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): promise])
.when { $0.groupImagePublishers }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher])
let promise2 = mockStorage.read { db in
let publisher2 = mockStorage.read { db in
OpenGroupManager
.roomImage(
db,
@ -3509,7 +3521,7 @@ class OpenGroupManagerSpec: QuickSpec {
using: dependencies
)
}
expect(promise2).to(equal(promise))
expect(publisher2).to(equal(publisher))
}
it("does not save the fetched image to storage") {

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,19 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Combine
import SessionUtilitiesKit
@testable import SessionMessagingKit
class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? {
get { return accept() as? Promise<[OpenGroupAPI.Room]> }
var defaultRoomsPublisher: AnyPublisher<[OpenGroupAPI.Room], Error>? {
get { return accept() as? AnyPublisher<[OpenGroupAPI.Room], Error> }
set { accept(args: [newValue]) }
}
var groupImagePromises: [String: Promise<Data>] {
get { return accept() as! [String: Promise<Data>] }
var groupImagePublishers: [String: AnyPublisher<Data, Error>] {
get { return accept() as! [String: AnyPublisher<Data, Error>] }
set { accept(args: [newValue]) }
}

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit

View File

@ -1,7 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Combine
import SessionSnodeKit
import SessionUtilitiesKit
@ -23,7 +23,7 @@ class TestOnionRequestAPI: OnionRequestAPIType {
}
}
}
class ResponseInfo: ResponseInfoType {
let requestData: RequestData
let code: Int
@ -38,41 +38,45 @@ class TestOnionRequestAPI: OnionRequestAPIType {
class var mockResponse: Data? { return nil }
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(ResponseInfoType, Data?)> {
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: request.url?.absoluteString,
httpMethod: (request.httpMethod ?? "GET"),
headers: (request.allHTTPHeaderFields ?? [:]),
body: request.httpBody,
destination: OnionRequestAPIDestination.server(
host: request.url!.host!,
target: OnionRequestAPIVersion.v4.rawValue,
x25519PublicKey: x25519PublicKey,
scheme: request.url!.scheme,
port: request.url!.port.map { UInt16($0) }
)
server: server,
version: .v4,
publicKey: x25519PublicKey
),
code: 200,
headers: [:]
)
return Promise.value((responseInfo, mockResponse))
return Just((responseInfo, mockResponse))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> Promise<(ResponseInfoType, Data?)> {
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: "\(snode.address):\(snode.port)/onion_req/v2",
urlString: nil,
httpMethod: "POST",
headers: [:],
snodeMethod: nil,
body: payload,
destination: OnionRequestAPIDestination.snode(snode)
server: "",
version: .v3,
publicKey: snode.x25519PublicKey
),
code: 200,
headers: [:]
)
return Promise.value((responseInfo, mockResponse))
return Just((responseInfo, mockResponse))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}

View File

@ -48,7 +48,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
AppReadiness.runNowOrWhenAppDidBecomeReady {
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups()
defer {
// TODO: Test this
Publishers
.MergeMany(openGroupPollingPublishers)
.sinkUntilComplete(

View File

@ -14,30 +14,24 @@ public protocol OnionRequestAPIType {
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI: OnionRequestAPIType {
private static var buildPathsPublisher: Atomic<AnyPublisher<[[Snode]], Error>?> = Atomic(nil)
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var pathFailureCount: [[Snode]: UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var snodeFailureCount: [Snode: UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var guardSnodes: Set<Snode> = []
private static var pathFailureCount: Atomic<[[Snode]: UInt]> = Atomic([:])
private static var snodeFailureCount: Atomic<[Snode: UInt]> = Atomic([:])
public static var guardSnodes: Atomic<Set<Snode>> = Atomic([])
// Not a set to ensure we consistently show the same path to the user
private static var _paths: [[Snode]]?
private static var _paths: Atomic<[[Snode]]?> = Atomic(nil)
public static var paths: [[Snode]] {
get {
if let paths: [[Snode]] = _paths { return paths }
if let paths: [[Snode]] = _paths.wrappedValue { return paths }
let results: [[Snode]]? = Storage.shared.read { db in
try? Snode.fetchAllOnionRequestPaths(db)
}
if results?.isEmpty == false { _paths = results }
if results?.isEmpty == false { _paths.mutate { $0 = results } }
return (results ?? [])
}
set { _paths = newValue }
set { _paths.mutate { $0 = newValue } }
}
// MARK: - Settings
@ -94,8 +88,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with
/// `Error.insufficientSnodes` if not enough (reliable) snodes are available.
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> AnyPublisher<Set<Snode>, Error> {
guard guardSnodes.count < targetGuardSnodeCount else {
return Just(guardSnodes)
guard guardSnodes.wrappedValue.count < targetGuardSnodeCount else {
return Just(guardSnodes.wrappedValue)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -141,7 +135,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.map { output in Set(output) }
.handleEvents(
receiveOutput: { output in
OnionRequestAPI.guardSnodes = output
OnionRequestAPI.guardSnodes.mutate { $0 = output }
}
)
.eraseToAnyPublisher()
@ -222,10 +216,12 @@ public enum OnionRequestAPI: OnionRequestAPIType {
var cancellable: [AnyCancellable] = []
if !paths.isEmpty {
guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= 2 {
guardSnodes.formUnion([ paths[1][0] ])
guardSnodes.mutate {
$0.formUnion([ paths[0][0] ])
if paths.count >= 2 {
$0.formUnion([ paths[1][0] ])
}
}
}
@ -309,20 +305,14 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
private static func dropGuardSnode(_ snode: Snode) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
guardSnodes = guardSnodes.filter { $0 != snode }
guardSnodes.mutate { snodes in snodes = snodes.filter { $0 != snode } }
}
private static func drop(_ snode: Snode) throws {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
// We repair the path here because we can do it sync. In the case where we drop a whole
// path we leave the re-building up to getPath(excluding:) because re-building the path
// in that case is async.
OnionRequestAPI.snodeFailureCount[snode] = 0
OnionRequestAPI.snodeFailureCount.mutate { $0[snode] = 0 }
var oldPaths = paths
guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return }
var path = oldPaths[pathIndex]
@ -344,10 +334,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
private static func drop(_ path: [Snode]) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
OnionRequestAPI.pathFailureCount[path] = 0
OnionRequestAPI.pathFailureCount.mutate { $0[path] = 0 }
var paths = OnionRequestAPI.paths
guard let pathIndex = paths.firstIndex(of: path) else { return }
paths.remove(at: pathIndex)
@ -533,7 +520,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
func handleUnspecificError() {
guard let path = path else { return }
var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0
var pathFailureCount: UInt = (OnionRequestAPI.pathFailureCount.wrappedValue[path] ?? 0)
pathFailureCount += 1
if pathFailureCount >= pathFailureThreshold {
@ -545,7 +532,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
drop(path)
}
else {
OnionRequestAPI.pathFailureCount[path] = pathFailureCount
OnionRequestAPI.pathFailureCount.mutate { $0[path] = pathFailureCount }
}
}
@ -566,7 +553,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..<message.endIndex]
if let path = path, let snode = path.first(where: { $0.ed25519PublicKey == ed25519PublicKey }) {
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?? 0
var snodeFailureCount: UInt = (OnionRequestAPI.snodeFailureCount.wrappedValue[snode] ?? 0)
snodeFailureCount += 1
if snodeFailureCount >= snodeFailureThreshold {
@ -579,7 +566,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
}
else {
OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
OnionRequestAPI.snodeFailureCount
.mutate { $0[snode] = snodeFailureCount }
}
} else {
// Do nothing

View File

@ -209,10 +209,6 @@ class ThreadSettingsViewModelSpec: QuickSpec {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
viewModel.textChanged("TestNew", for: .nickname)
// TODO: Enter edit mode by pressing on the first item
// viewModel.tableData.first?
// .elements.first?
// .onTap?()
}
it("enters the editing state") {
@ -337,12 +333,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
context("when entering edit mode") {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
viewModel.textChanged("TestUserNew", for: .nickname)
// TODO: Enter edit mode by pressing on the first item
// viewModel.tableData.first?
// .elements.first?
// .onTap?()
viewModel.textChanged("TestNew", for: .nickname)
}
it("enters the editing state") {

View File

@ -20,6 +20,11 @@ extension Optional {
public func defaulting(to value: Wrapped) -> Wrapped {
return (self ?? value)
}
public mutating func setting(to value: Wrapped) -> Wrapped {
self = value
return value
}
}
extension Optional where Wrapped == String {

View File

@ -1,18 +1,17 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Combine
import Quick
import Nimble
@testable import SessionUtilitiesKit
class BatchRequestInfoSpec: QuickSpec {
class BatchResponseSpec: QuickSpec {
struct TestType: Codable, Equatable {
let stringValue: String
}
struct TestType2: Codable, Equatable {
let intValue: Int
let stringValue2: String
@ -136,9 +135,9 @@ class BatchRequestInfoSpec: QuickSpec {
}
}
// MARK: - --Promise
// MARK: - --Combine
describe("a (ResponseInfoType, Data?) Promise") {
describe("a (ResponseInfoType, Data?) Publisher") {
var responseInfo: ResponseInfoType!
var testType: TestType!
var testType2: TestType2!
@ -146,8 +145,8 @@ class BatchRequestInfoSpec: QuickSpec {
beforeEach {
responseInfo = HTTP.ResponseInfo(code: 200, headers: [:])
testType = TestType(stringValue: "Test")
testType2 = TestType2(intValue: 1, stringValue2: "Test2")
testType = TestType(stringValue: "test1")
testType2 = TestType2(intValue: 123, stringValue2: "test2")
data = """
[\([
try! JSONEncoder().encode(
@ -173,46 +172,79 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("decodes valid data correctly") {
let result = Promise.value((responseInfo, data))
var result: HTTP.BatchResponse?
Just((responseInfo, data))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self
])
.sinkUntilComplete(
receiveValue: { result = $0 }
)
expect(result.value).toNot(beNil())
expect((result.value?[0].1 as? HTTP.BatchSubResponse<TestType>)?.body)
expect(result).toNot(beNil())
expect((result?[0].1 as? HTTP.BatchSubResponse<TestType>)?.body)
.to(equal(testType))
expect((result.value?[1].1 as? HTTP.BatchSubResponse<TestType2>)?.body)
expect((result?[1].1 as? HTTP.BatchSubResponse<TestType2>)?.body)
.to(equal(testType2))
}
it("fails if there is no data") {
let result = Promise.value((responseInfo, nil)).decoded(as: [])
var error: Error?
Just((responseInfo, nil))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [])
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
expect(error?.localizedDescription)
.to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the data is not JSON") {
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
var error: Error?
Just((responseInfo, Data([1, 2, 3])))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [])
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
expect(error?.localizedDescription)
.to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the data is not a JSON array") {
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
var error: Error?
Just((responseInfo, "{}".data(using: .utf8)))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [])
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
expect(error?.localizedDescription)
.to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the JSON array does not have the same number of items as the expected types") {
let result = Promise.value((responseInfo, data))
var error: Error?
Just((responseInfo, data))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self,
HTTP.BatchSubResponse<TestType2>.self
])
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
expect(error?.localizedDescription)
.to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if one of the JSON array values fails to decode") {
@ -230,13 +262,20 @@ class BatchRequestInfoSpec: QuickSpec {
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ",")),{"test": "test"}]
""".data(using: .utf8)!
let result = Promise.value((responseInfo, data))
var error: Error?
Just((responseInfo, data))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self
])
.mapError { error.setting(to: $0) }
.sinkUntilComplete()
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
expect(error?.localizedDescription)
.to(equal(HTTPError.parsingFailed.localizedDescription))
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
import Quick
import Nimble

View File

@ -1,28 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import PromiseKit
import SessionUtilitiesKit
class SynchronousStorage: Storage {
override func writeAsync<T>(updates: @escaping (Database) throws -> T) {
super.write(updates: updates)
}
override func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void) {
super.write { db in
do {
var result: T?
try db.inTransaction {
result = try updates(db)
return .commit
}
try? completion(db, .success(result!))
}
catch {
try? completion(db, .failure(error))
}
override func writePublisher<T>(updates: @escaping (Database) throws -> T) -> AnyPublisher<T, Error> {
guard let result: T = super.write(updates: updates) else {
return Fail(error: StorageError.generic)
.eraseToAnyPublisher()
}
return Just(result)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}