diff --git a/Podfile b/Podfile index c903f9d7d..fc32f6793 100644 --- a/Podfile +++ b/Podfile @@ -56,12 +56,20 @@ abstract_target 'GlobalDependencies' do target 'SessionMessagingKitTests' do inherit! :complete + pod 'Quick' pod 'Nimble' end end target 'SessionUtilitiesKit' do pod 'SAMKeychain' + + target 'SessionUtilitiesKitTests' do + inherit! :complete + + pod 'Quick' + pod 'Nimble' + end end end end diff --git a/Podfile.lock b/Podfile.lock index f504ecaa4..a4d945ac0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,6 +39,7 @@ PODS: - PromiseKit/UIKit (6.15.3): - PromiseKit/CorePromise - PureLayout (3.1.9) + - Quick (4.0.0) - Reachability (3.2) - SAMKeychain (1.5.3) - SignalCoreKit (1.0.0): @@ -129,6 +130,7 @@ DEPENDENCIES: - NVActivityIndicatorView - PromiseKit - PureLayout (~> 3.1.8) + - Quick - Reachability - SAMKeychain - SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`) @@ -148,6 +150,7 @@ SPEC REPOS: - OpenSSL-Universal - PromiseKit - PureLayout + - Quick - Reachability - SAMKeychain - SQLCipher @@ -204,6 +207,7 @@ SPEC CHECKSUMS: OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2 PromiseKit: 3b2b6995e51a954c46dbc550ce3da44fbfb563c5 PureLayout: 5fb5e5429519627d60d079ccb1eaa7265ce7cf88 + Quick: 6473349e43b9271a8d43839d9ba1c442ed1b7ac4 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d @@ -214,6 +218,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 2cc64d50f25c3b1627c3e958ae50e25fead25564 +PODFILE CHECKSUM: b95d8bb031996cffdb5d9b9b49bce3b24d6026d7 COCOAPODS: 1.11.2 diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift index 3c5d7de12..6cf98b51d 100644 --- a/SessionMessagingKit/Open Groups/Models/Capabilities.swift +++ b/SessionMessagingKit/Open Groups/Models/Capabilities.swift @@ -24,7 +24,7 @@ extension OpenGroupAPI { } } - // MARK: - Codable + // MARK: - Initialization public init(from valueString: String) { let maybeValue: Capability? = Capability.allCases.first { $0.rawValue == valueString } diff --git a/SessionMessagingKit/Open Groups/Models/OpenGroup.swift b/SessionMessagingKit/Open Groups/Models/OpenGroup.swift index 2f2ba12a1..36a6c4398 100644 --- a/SessionMessagingKit/Open Groups/Models/OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Models/OpenGroup.swift @@ -63,5 +63,5 @@ public final class OpenGroup: NSObject, NSCoding { // NSObject/NSCoding conforma coder.encode(infoUpdates, forKey: "infoUpdates") } - override public var description: String { "\(name) (Server: \(server), Room: \(room)" } + override public var description: String { "\(name) (Server: \(server), Room: \(room))" } } diff --git a/SessionMessagingKit/Utilities/ContactUtilities.swift b/SessionMessagingKit/Utilities/ContactUtilities.swift index 704fb3813..25e4e3cda 100644 --- a/SessionMessagingKit/Utilities/ContactUtilities.swift +++ b/SessionMessagingKit/Utilities/ContactUtilities.swift @@ -1,6 +1,6 @@ import SessionUtilitiesKit -enum ContactUtilities { +public enum ContactUtilities { private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? { guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil } guard thread.shouldBeVisible else { return nil } @@ -12,7 +12,7 @@ enum ContactUtilities { return contact } - static func getAllContacts() -> [String] { + public static func getAllContacts() -> [String] { // Collect all contacts var result: [Contact] = [] Storage.read { transaction in @@ -39,7 +39,7 @@ enum ContactUtilities { .map { $0.sessionID } } - static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { + public static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { Storage.read { transaction in TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in guard let contactThread: TSContactThread = object as? TSContactThread else { return } @@ -50,7 +50,7 @@ enum ContactUtilities { } } - static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? { + public static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? { // TODO: Ensure the above case isn't going to be an issue due to legacy messages?. // Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard // sessionId, as a result in order to see if there is an unblinded contact for this blindedId we diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift new file mode 100644 index 000000000..707f8852d --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift @@ -0,0 +1,97 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class CapabilitiesSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("Capabilities") { + context("when initializing") { + it("assigns values correctly") { + let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( + capabilities: [.sogs], + missing: [.sogs] + ) + + expect(capabilities.capabilities).to(equal([.sogs])) + expect(capabilities.missing).to(equal([.sogs])) + } + + it("defaults missing to nil") { + let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( + capabilities: [.sogs] + ) + + expect(capabilities.capabilities).to(equal([.sogs])) + expect(capabilities.missing).to(beNil()) + } + } + } + + describe("a Capability") { + context("when initializing") { + it("succeeeds with a valid case") { + let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + from: "sogs" + ) + + expect(capability).to(equal(.sogs)) + } + + it("wraps an unknown value in the unsupported case") { + let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + from: "test" + ) + + expect(capability).to(equal(.unsupported("test"))) + } + } + + context("when accessing the rawValue") { + it("provides known cases exactly") { + expect(OpenGroupAPI.Capabilities.Capability.sogs.rawValue).to(equal("sogs")) + expect(OpenGroupAPI.Capabilities.Capability.blind.rawValue).to(equal("blind")) + } + + it("provides the wrapped value for unsupported cases") { + expect(OpenGroupAPI.Capabilities.Capability.unsupported("test").rawValue).to(equal("test")) + } + } + + context("when Decoding") { + it("decodes known cases exactly") { + expect( + try? JSONDecoder().decode( + OpenGroupAPI.Capabilities.Capability.self, + from: "\"sogs\"".data(using: .utf8)! + ) + ) + .to(equal(.sogs)) + expect( + try? JSONDecoder().decode( + OpenGroupAPI.Capabilities.Capability.self, + from: "\"blind\"".data(using: .utf8)! + ) + ) + .to(equal(.blind)) + } + + it("decodes unknown cases into the unsupported case") { + expect( + try? JSONDecoder().decode( + OpenGroupAPI.Capabilities.Capability.self, + from: "\"test\"".data(using: .utf8)! + ) + ) + .to(equal(.unsupported("test"))) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift new file mode 100644 index 000000000..b36ce7a48 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift @@ -0,0 +1,76 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class OpenGroupSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("an Open Group") { + context("when initializing") { + it("generates the id") { + let openGroup: OpenGroup = OpenGroup( + server: "server", + room: "room", + publicKey: "1234", + name: "name", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + + expect(openGroup.id).to(equal("server.room")) + } + } + + context("when NSCoding") { + // Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable + it("successfully encodes and decodes") { + let openGroupToEncode: OpenGroup = OpenGroup( + server: "server", + room: "room", + publicKey: "1234", + name: "name", + groupDescription: "desc", + imageID: "image", + infoUpdates: 1 + ) + let encodedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: openGroupToEncode, requiringSecureCoding: false) + let openGroup: OpenGroup? = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData) as? OpenGroup + + expect(openGroup).toNot(beNil()) + expect(openGroup?.id).to(equal("server.room")) + expect(openGroup?.server).to(equal("server")) + expect(openGroup?.room).to(equal("room")) + expect(openGroup?.publicKey).to(equal("1234")) + expect(openGroup?.name).to(equal("name")) + expect(openGroup?.groupDescription).to(equal("desc")) + expect(openGroup?.imageID).to(equal("image")) + expect(openGroup?.infoUpdates).to(equal(1)) + } + } + + context("when describing") { + it("includes relevant information") { + let openGroup: OpenGroup = OpenGroup( + server: "server", + room: "room", + publicKey: "1234", + name: "name", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + + expect(openGroup.description) + .to(equal("name (Server: server, Room: room)")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift b/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift new file mode 100644 index 000000000..ccd8557d0 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift @@ -0,0 +1,74 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class ServerSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("an Open Group Server") { + context("when initializing") { + it("converts the server name to lowercase") { + let server: OpenGroupAPI.Server = OpenGroupAPI.Server( + name: "TeSt", + capabilities: OpenGroupAPI.Capabilities(capabilities: [], missing: nil) + ) + + expect(server.name).to(equal("test")) + } + } + + context("when NSCoding") { + // Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable + it("successfully encodes and decodes") { + let serverToEncode: OpenGroupAPI.Server = OpenGroupAPI.Server( + name: "test", + capabilities: OpenGroupAPI.Capabilities( + capabilities: [.sogs, .unsupported("other")], + missing: [.blind, .unsupported("other2")]) + ) + let encodedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: serverToEncode, requiringSecureCoding: false) + let server: OpenGroupAPI.Server? = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData) as? OpenGroupAPI.Server + + expect(server).toNot(beNil()) + expect(server?.name).to(equal("test")) + expect(server?.capabilities.capabilities).to(equal([.sogs, .unsupported("other")])) + expect(server?.capabilities.missing).to(equal([.blind, .unsupported("other2")])) + } + } + + context("when describing") { + it("includes relevant information") { + let server: OpenGroupAPI.Server = OpenGroupAPI.Server( + name: "TeSt", + capabilities: OpenGroupAPI.Capabilities( + capabilities: [.sogs, .unsupported("other")], + missing: [.blind, .unsupported("other2")] + ) + ) + + expect(server.description) + .to(equal("test (Capabilities: [sogs, other], Missing: [blind, other2])")) + } + + it("handles nil missing capabilities") { + let server: OpenGroupAPI.Server = OpenGroupAPI.Server( + name: "TeSt", + capabilities: OpenGroupAPI.Capabilities( + capabilities: [.sogs, .unsupported("other")], + missing: nil + ) + ) + + expect(server.description) + .to(equal("test (Capabilities: [sogs, other], Missing: [])")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift new file mode 100644 index 000000000..31dd50c17 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -0,0 +1,655 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import PromiseKit +import Sodium +import SessionSnodeKit + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class OpenGroupAPISpec: QuickSpec { + class TestResponseInfo: OnionRequestResponseInfoType { + let requestData: TestApi.RequestData + let code: Int + let headers: [String: String] + + init(requestData: TestApi.RequestData, code: Int, headers: [String: String]) { + self.requestData = requestData + self.code = code + self.headers = headers + } + } + + struct TestNonce16Generator: NonceGenerator16ByteType { + var NonceBytes: Int = 16 + + func nonce() -> Array { return Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes } + } + + struct TestNonce24Generator: NonceGenerator24ByteType { + var NonceBytes: Int = 24 + + func nonce() -> Array { return Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes } + } + + class TestApi: OnionRequestAPIType { + struct RequestData: Codable { + let urlString: String? + let httpMethod: String + let headers: [String: String] + let snodeMethod: String? + let body: Data? + + let server: String + let version: OnionRequestAPI.Version + let publicKey: String? + } + + class var mockResponse: Data? { return nil } + + static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let responseInfo: TestResponseInfo = TestResponseInfo( + requestData: RequestData( + urlString: request.url?.absoluteString, + httpMethod: (request.httpMethod ?? "GET"), + headers: (request.allHTTPHeaderFields ?? [:]), + snodeMethod: nil, + body: request.httpBody, + + server: server, + version: version, + publicKey: x25519PublicKey + ), + code: 200, + headers: [:] + ) + + return Promise.value((responseInfo, mockResponse)) + } + + static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise { + // TODO: Test the 'responseInfo' somehow? + return Promise.value(mockResponse!) + } + } + + // MARK: - Spec + + override func spec() { + var testStorage: TestStorage! + var testSodium: TestSodium! + var testAeadXChaCha20Poly1305Ietf: TestAeadXChaCha20Poly1305Ietf! + var testGenericHash: TestGenericHash! + var testSign: TestSign! + var dependencies: OpenGroupAPI.Dependencies! + + var response: (OnionRequestResponseInfoType, Codable)? = nil + var pollResponse: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? + var error: Error? + + describe("an OpenGroupAPI") { + // MARK: - Configuration + + beforeEach { + testStorage = TestStorage() + testSodium = TestSodium() + testAeadXChaCha20Poly1305Ietf = TestAeadXChaCha20Poly1305Ietf() + testGenericHash = TestGenericHash() + testSign = TestSign() + dependencies = OpenGroupAPI.Dependencies( + api: TestApi.self, + storage: testStorage, + sodium: testSodium, + aeadXChaCha20Poly1305Ietf: testAeadXChaCha20Poly1305Ietf, + sign: testSign, + genericHash: testGenericHash, + ed25519: TestEd25519.self, + nonceGenerator16: TestNonce16Generator(), + nonceGenerator24: TestNonce24Generator(), + date: Date(timeIntervalSince1970: 1234567890) + ) + + testStorage.mockData[.allOpenGroups] = [ + "0": OpenGroup( + server: "testServer", + room: "testRoom", + publicKey: TestConstants.publicKey, + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ] + testStorage.mockData[.openGroupPublicKeys] = [ + "testServer": TestConstants.publicKey + ] + testStorage.mockData[.userKeyPair] = try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ) + testStorage.mockData[.userEdKeyPair] = Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ) + + testGenericHash.mockData[.hashOutputLength] = [] + testSodium.mockData[.blindedKeyPair] = Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ) + testSodium.mockData[.sogsSignature] = "TestSogsSignature".bytes + testSign.mockData[.signature] = "TestSignature".bytes + } + + afterEach { + dependencies = nil + testStorage = nil + response = nil + pollResponse = nil + } + + // MARK: - Batching & Polling + + context("when polling") { + it("generates the correct request") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: try! JSONDecoder().decode( + OpenGroupAPI.RoomPollInfo.self, + from: """ + { + \"token\":\"test\", + \"active_users\":1, + \"read\":true, + \"write\":true, + \"upload\":true + } + """.data(using: .utf8)! + ), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.Message](), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.DirectMessage](), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.DirectMessage](), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(pollResponse?.values).to(haveCount(5)) + expect(pollResponse?.keys).to(contain(.capabilities)) + expect(pollResponse?.keys).to(contain(.roomPollInfo("testRoom", 0))) + expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) + expect(pollResponse?.keys).to(contain(.inbox)) + expect(pollResponse?.keys).to(contain(.outbox)) + expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + + // Validate request data + let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/batch")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + } + + it("errors when no data is returned") { + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when invalid data is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an empty array is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "[]".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an empty object is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "{}".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when a different number of responses are returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: try! JSONDecoder().decode( + OpenGroupAPI.RoomPollInfo.self, + from: """ + { + \"token\":\"test\", + \"active_users\":1, + \"read\":true, + \"write\":true, + \"upload\":true + } + """.data(using: .utf8)! + ), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an unexpected response is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + } + + // MARK: - Files + + context("when uploading files") { + it("doesn't add a fileName header when not provided") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: 1)) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.headers).to(haveCount(4)) + expect(requestData?.headers.keys).toNot(contain(Header.fileName.rawValue)) + } + + it("adds a fileName header when provided") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: 1)) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.headers).to(haveCount(5)) + expect(requestData?.headers[Header.fileName.rawValue]).to(equal("TestFileName")) + } + } + + // MARK: - Authentication + + context("when signing") { + beforeEach { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode([OpenGroupAPI.Room]()) + } + } + + dependencies = dependencies.with(api: LocalTestApi.self) + } + + it("fails when there is no userEdKeyPair") { + testStorage.mockData[.userEdKeyPair] = nil + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails when there is no serverPublicKey") { + testStorage.mockData[.openGroupPublicKeys] = [:] + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.noPublicKey.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + context("when unblinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) + ) + } + + it("signs correctly") { + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/rooms")) + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers).to(haveCount(4)) + expect(requestData?.headers[Header.sogsPubKey.rawValue]) + .to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) + expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) + expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64())) + } + + it("fails when the signature is not generated") { + testSign.mockData[.signature] = nil + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + + context("when blinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) + ) + } + + it("signs correctly") { + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/rooms")) + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers).to(haveCount(4)) + expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) + expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) + expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64())) + } + + it("fails when the blindedKeyPair is not generated") { + testSodium.mockData[.blindedKeyPair] = nil + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails when the sogsSignature is not generated") { + testSodium.mockData[.sogsSignature] = nil + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift deleted file mode 100644 index 566769417..000000000 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift +++ /dev/null @@ -1,727 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import XCTest -import Nimble -import PromiseKit -import Sodium -import SessionSnodeKit - -@testable import SessionMessagingKit - -class OpenGroupAPITests: XCTestCase { - class TestResponseInfo: OnionRequestResponseInfoType { - let requestData: TestApi.RequestData - let code: Int - let headers: [String: String] - - init(requestData: TestApi.RequestData, code: Int, headers: [String: String]) { - self.requestData = requestData - self.code = code - self.headers = headers - } - } - - struct TestNonce16Generator: NonceGenerator16ByteType { - var NonceBytes: Int = 16 - - func nonce() -> Array { return Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes } - } - - struct TestNonce24Generator: NonceGenerator24ByteType { - var NonceBytes: Int = 24 - - func nonce() -> Array { return Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes } - } - - class TestApi: OnionRequestAPIType { - struct RequestData: Codable { - let urlString: String? - let httpMethod: String - let headers: [String: String] - let snodeMethod: String? - let body: Data? - - let server: String - let version: OnionRequestAPI.Version - let publicKey: String? - } - - class var mockResponse: Data? { return nil } - - static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { - let responseInfo: TestResponseInfo = TestResponseInfo( - requestData: RequestData( - urlString: request.url?.absoluteString, - httpMethod: (request.httpMethod ?? "GET"), - headers: (request.allHTTPHeaderFields ?? [:]), - snodeMethod: nil, - body: request.httpBody, - - server: server, - version: version, - publicKey: x25519PublicKey - ), - code: 200, - headers: [:] - ) - - return Promise.value((responseInfo, mockResponse)) - } - - static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise { - // TODO: Test the 'responseInfo' somehow? - return Promise.value(mockResponse!) - } - } - - var testStorage: TestStorage! - var testSodium: TestSodium! - var testAeadXChaCha20Poly1305Ietf: TestAeadXChaCha20Poly1305Ietf! - var testGenericHash: TestGenericHash! - var testSign: TestSign! - var dependencies: OpenGroupAPI.Dependencies! - - // MARK: - Configuration - - override func setUpWithError() throws { - testStorage = TestStorage() - testSodium = TestSodium() - testAeadXChaCha20Poly1305Ietf = TestAeadXChaCha20Poly1305Ietf() - testGenericHash = TestGenericHash() - testSign = TestSign() - dependencies = OpenGroupAPI.Dependencies( - api: TestApi.self, - storage: testStorage, - sodium: testSodium, - aeadXChaCha20Poly1305Ietf: testAeadXChaCha20Poly1305Ietf, - sign: testSign, - genericHash: testGenericHash, - ed25519: TestEd25519.self, - nonceGenerator16: TestNonce16Generator(), - nonceGenerator24: TestNonce24Generator(), - date: Date(timeIntervalSince1970: 1234567890) - ) - - testStorage.mockData[.allOpenGroups] = [ - "0": OpenGroup( - server: "testServer", - room: "testRoom", - publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", - name: "Test", - groupDescription: nil, - imageID: nil, - infoUpdates: 0 - ) - ] - testStorage.mockData[.openGroupPublicKeys] = [ - "testServer": "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d" - ] - - // Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb) - testStorage.mockData[.userKeyPair] = try! ECKeyPair( - publicKeyData: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!, - privateKeyData: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")! - ) - testStorage.mockData[.userEdKeyPair] = Box.KeyPair( - publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes, - secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes - ) - - testGenericHash.mockData[.hashOutputLength] = [] - testSodium.mockData[.blindedKeyPair] = Box.KeyPair( - publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes, - secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes - ) - testSodium.mockData[.sogsSignature] = "TestSogsSignature".bytes - testSign.mockData[.signature] = "TestSignature".bytes - } - - override func tearDownWithError() throws { - dependencies = nil - testStorage = nil - } - - // MARK: - Batching & Polling - - func testPollGeneratesTheCorrectRequest() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: try! JSONDecoder().decode( - OpenGroupAPI.RoomPollInfo.self, - from: """ - { - \"token\":\"test\", - \"active_users\":1, - \"read\":true, - \"write\":true, - \"upload\":true - } - """.data(using: .utf8)! - ), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: [OpenGroupAPI.Message](), - failedToParseBody: false - ) - ) - ] - - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) - } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(response) - .toEventuallyNot( - beNil(), - timeout: .milliseconds(100) - ) - expect(error?.localizedDescription).to(beNil()) - - // Validate the response data - expect(response?.values).to(haveCount(3)) - expect(response?.keys).to(contain(.capabilities)) - expect(response?.keys).to(contain(.roomPollInfo("testRoom", 0))) - expect(response?.keys).to(contain(.roomMessagesRecent("testRoom"))) - expect(response?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) - - // Validate request data - let requestData: TestApi.RequestData? = (response?[.capabilities]?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/batch")) - expect(requestData?.httpMethod).to(equal("POST")) - expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) - } - - func testPollReturnsAnErrorWhenGivenNoData() throws { - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testPollReturnsAnErrorWhenGivenInvalidData() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { return Data() } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testPollReturnsAnErrorWhenGivenAnEmptyResponse() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { return "[]".data(using: .utf8) } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testPollReturnsAnErrorWhenGivenAnObjectResponse() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { return "{}".data(using: .utf8) } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testPollReturnsAnErrorWhenGivenAnDifferentNumberOfResponses() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: try! JSONDecoder().decode( - OpenGroupAPI.RoomPollInfo.self, - from: """ - { - \"token\":\"test\", - \"active_users\":1, - \"read\":true, - \"write\":true, - \"upload\":true - } - """.data(using: .utf8)! - ), - failedToParseBody: false - ) - ) - ] - - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) - } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testPollReturnsAnErrorWhenGivenAnUnexpectedResponse() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ) - ] - - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) - } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? = nil - var error: Error? = nil - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - // MARK: - Files - - func testItDoesNotAddAFileNameHeaderWhenNotProvided() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - return try! JSONEncoder().encode(FileUploadResponse(id: 1)) - } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: (OnionRequestResponseInfoType, FileUploadResponse)? = nil - var error: Error? = nil - - OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(response) - .toEventuallyNot( - beNil(), - timeout: .milliseconds(100) - ) - expect(error?.localizedDescription).to(beNil()) - - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) - expect(requestData?.httpMethod).to(equal("POST")) - expect(requestData?.headers).to(haveCount(4)) - expect(requestData?.headers.keys).toNot(contain(Header.fileName.rawValue)) - } - - func testItAddsAFileNameHeaderWhenProvided() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - return try! JSONEncoder().encode(FileUploadResponse(id: 1)) - } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: (OnionRequestResponseInfoType, FileUploadResponse)? = nil - var error: Error? = nil - - OpenGroupAPI.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(response) - .toEventuallyNot( - beNil(), - timeout: .milliseconds(100) - ) - expect(error?.localizedDescription).to(beNil()) - - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) - expect(requestData?.httpMethod).to(equal("POST")) - expect(requestData?.headers).to(haveCount(5)) - expect(requestData?.headers[Header.fileName.rawValue]).to(equal("TestFileName")) - } - - // MARK: - Authentication - - func testItSignsTheUnblindedRequestCorrectly() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - return try! JSONEncoder().encode([OpenGroupAPI.Room]()) - } - } - testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( - name: "testServer", - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) - ) - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: (OnionRequestResponseInfoType, [OpenGroupAPI.Room])? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(response) - .toEventuallyNot( - beNil(), - timeout: .milliseconds(100) - ) - expect(error?.localizedDescription).to(beNil()) - - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/rooms")) - expect(requestData?.httpMethod).to(equal("GET")) - expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) - expect(requestData?.headers).to(haveCount(4)) - expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) - expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) - expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) - expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64())) - } - - func testItSignsTheBlindedRequestCorrectly() throws { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - return try! JSONEncoder().encode([OpenGroupAPI.Room]()) - } - } - testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( - name: "testServer", - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) - ) - dependencies = dependencies.with(api: LocalTestApi.self) - - var response: (OnionRequestResponseInfoType, [OpenGroupAPI.Room])? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(response) - .toEventuallyNot( - beNil(), - timeout: .milliseconds(100) - ) - expect(error?.localizedDescription).to(beNil()) - - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/rooms")) - expect(requestData?.httpMethod).to(equal("GET")) - expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) - expect(requestData?.headers).to(haveCount(4)) - expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) - expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) - expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) - expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64())) - } - - func testItFailsToSignIfThereIsNoUserEdKeyPair() throws { - testStorage.mockData[.userEdKeyPair] = nil - - var response: Any? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.signingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testItFailsToSignIfTheServerPublicKeyIsInvalid() throws { - testStorage.mockData[.openGroupPublicKeys] = [:] - - var response: Any? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.noPublicKey.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testItFailsToSignIfBlindedAndTheBlindedKeyDoesNotGetGenerated() throws { - class InvalidSodium: SodiumType { - func getGenericHash() -> GenericHashType { return Sodium().genericHash } - func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return Sodium().aead.xchacha20poly1305ietf } - func getSign() -> SignType { return Sodium().sign } - - func generateBlindingFactor(serverPublicKey: String) -> Bytes? { return nil } - func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? { - return nil - } - func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? { - return nil - } - - func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? { return nil } - func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? { - return nil - } - - func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool { - return false - } - } - testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( - name: "testServer", - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) - ) - dependencies = dependencies.with(sodium: InvalidSodium()) - - var response: Any? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.signingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testItFailsToSignIfBlindedAndTheSogsSignatureDoesNotGetGenerated() throws { - class InvalidSodium: SodiumType { - func getGenericHash() -> GenericHashType { return Sodium().genericHash } - func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return Sodium().aead.xchacha20poly1305ietf } - func getSign() -> SignType { return Sodium().sign } - - func generateBlindingFactor(serverPublicKey: String) -> Bytes? { return nil } - func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? { - return Box.KeyPair( - publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes, - secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes - ) - } - func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? { - return nil - } - - func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? { return nil } - func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? { - return nil - } - - func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool { - return false - } - } - testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( - name: "testServer", - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) - ) - dependencies = dependencies.with(sodium: InvalidSodium()) - - var response: Any? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.signingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } - - func testItFailsToSignIfUnblindedAndTheSignatureDoesNotGetGenerated() throws { - class InvalidSign: SignType { - var PublicKeyBytes: Int = 32 - - func signature(message: Bytes, secretKey: Bytes) -> Bytes? { return nil } - func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool { return false } - func toX25519(ed25519PublicKey: Bytes) -> Bytes? { return nil } - } - testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( - name: "testServer", - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) - ) - dependencies = dependencies.with(sign: InvalidSign()) - - var response: Any? = nil - var error: Error? = nil - - OpenGroupAPI.rooms(for: "testServer", using: dependencies) - .get { result in response = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(OpenGroupAPI.Error.signingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(response).to(beNil()) - } -} diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift index 494cf03c2..08a0ebfbc 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -19,7 +19,8 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { case openGroupImage case openGroupUserCount case openGroupSequenceNumber - case openGroupLatestMessageId + case openGroupInboxLatestMessageId + case openGroupOutboxLatestMessageId } typealias Key = DataKey @@ -50,6 +51,19 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { func getUser() -> Contact? { return nil } func getAllContacts() -> Set { return Set() } func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set { return Set() } + + // MARK: - Blinded Id cache + + func getBlindedIdMapping(with blindedId: String) -> BlindedIdMapping? { return nil } + func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping? { + return nil + } + + func cacheBlindedIdMapping(_ mapping: BlindedIdMapping) {} + func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction) {} + func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> ()) {} + func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> (), transaction: YapDatabaseReadTransaction) { + } // MARK: - Closed Groups @@ -111,20 +125,37 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { } func getOpenGroupInboxLatestMessageId(for server: String) -> Int64? { - let data: [String: Int64] = ((mockData[.openGroupLatestMessageId] as? [String: Int64]) ?? [:]) + let data: [String: Int64] = ((mockData[.openGroupInboxLatestMessageId] as? [String: Int64]) ?? [:]) return data[server] } func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) { - var updatedData: [String: Int64] = ((mockData[.openGroupLatestMessageId] as? [String: Int64]) ?? [:]) + var updatedData: [String: Int64] = ((mockData[.openGroupInboxLatestMessageId] as? [String: Int64]) ?? [:]) updatedData[server] = newValue - mockData[.openGroupLatestMessageId] = updatedData + mockData[.openGroupInboxLatestMessageId] = updatedData } func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any) { - var updatedData: [String: Int64] = ((mockData[.openGroupLatestMessageId] as? [String: Int64]) ?? [:]) + var updatedData: [String: Int64] = ((mockData[.openGroupInboxLatestMessageId] as? [String: Int64]) ?? [:]) + updatedData[server] = nil + mockData[.openGroupInboxLatestMessageId] = updatedData + } + + func getOpenGroupOutboxLatestMessageId(for server: String) -> Int64? { + let data: [String: Int64] = ((mockData[.openGroupOutboxLatestMessageId] as? [String: Int64]) ?? [:]) + return data[server] + } + + func setOpenGroupOutboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) { + var updatedData: [String: Int64] = ((mockData[.openGroupOutboxLatestMessageId] as? [String: Int64]) ?? [:]) + updatedData[server] = newValue + mockData[.openGroupOutboxLatestMessageId] = updatedData + } + + func removeOpenGroupOutboxLatestMessageId(for server: String, using transaction: Any) { + var updatedData: [String: Int64] = ((mockData[.openGroupOutboxLatestMessageId] as? [String: Int64]) ?? [:]) updatedData[server] = nil - mockData[.openGroupLatestMessageId] = updatedData + mockData[.openGroupOutboxLatestMessageId] = updatedData } // MARK: - Open Group Public Keys diff --git a/SessionUtilitiesKit/General/SessionId.swift b/SessionUtilitiesKit/General/SessionId.swift index a20c25620..3f81bc6b6 100644 --- a/SessionUtilitiesKit/General/SessionId.swift +++ b/SessionUtilitiesKit/General/SessionId.swift @@ -43,13 +43,6 @@ public struct SessionId { self.publicKey = idString.substring(from: 2) } - public init?(_ type: Prefix, publicKey: String) { - guard ECKeyPair.isValidHexEncodedPublicKey(candidate: publicKey) else { return nil } - - self.prefix = type - self.publicKey = publicKey - } - public init(_ type: Prefix, publicKey: Bytes) { self.prefix = type self.publicKey = publicKey.map { String(format: "%02hhx", $0) }.joined() diff --git a/SessionUtilitiesKitTests/General/SessionIdSpec.swift b/SessionUtilitiesKitTests/General/SessionIdSpec.swift new file mode 100644 index 000000000..99124712a --- /dev/null +++ b/SessionUtilitiesKitTests/General/SessionIdSpec.swift @@ -0,0 +1,87 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class SessionIdSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SessionId") { + context("when initializing") { + context("with an idString") { + it("succeeds when correct") { + let sessionId: SessionId? = SessionId(from: "05\(TestConstants.publicKey)") + + expect(sessionId?.prefix).to(equal(.standard)) + expect(sessionId?.publicKey).to(equal(TestConstants.publicKey)) + } + + it("fails when too short") { + expect(SessionId(from: "")).to(beNil()) + } + + it("fails with an invalid prefix") { + expect(SessionId(from: "AB\(TestConstants.publicKey)")).to(beNil()) + } + } + + context("with a prefix and publicKey") { + it("converts the bytes into a hex string") { + let sessionId: SessionId? = SessionId(.standard, publicKey: [0, 1, 2, 3, 4, 5, 6, 7, 8]) + + expect(sessionId?.prefix).to(equal(.standard)) + expect(sessionId?.publicKey).to(equal("000102030405060708")) + } + } + } + + it("generates the correct hex string") { + expect(SessionId(.unblinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) + .to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(SessionId(.standard, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) + .to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(SessionId(.blinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) + .to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + } + } + + describe("a SessionId Prefix") { + context("when initializing") { + context("with just a prefix") { + it("succeeds when valid") { + expect(SessionId.Prefix(from: "00")).to(equal(.unblinded)) + expect(SessionId.Prefix(from: "05")).to(equal(.standard)) + expect(SessionId.Prefix(from: "15")).to(equal(.blinded)) + } + + it("fails when nil") { + expect(SessionId.Prefix(from: nil)).to(beNil()) + } + + it("fails when invalid") { + expect(SessionId.Prefix(from: "AB")).to(beNil()) + } + } + + context("with a longer string") { + it("fails with invalid hex") { + expect(SessionId.Prefix(from: "Hello!!!")).to(beNil()) + } + + it("fails with the wrong length") { + expect(SessionId.Prefix(from: String(TestConstants.publicKey.prefix(10)))).to(beNil()) + } + + it("fails with an invalid prefix") { + expect(SessionId.Prefix(from: "AB\(TestConstants.publicKey)")).to(beNil()) + } + } + } + } + } +} diff --git a/SharedTest/TestConstants.swift b/SharedTest/TestConstants.swift new file mode 100644 index 000000000..3e2bb9ef0 --- /dev/null +++ b/SharedTest/TestConstants.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum TestConstants { + // Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb) + static let publicKey: String = "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d" + static let privateKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4" + static let edSecretKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4" +}