session-ios/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift

3684 lines
176 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import Sodium
import SessionSnodeKit
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
class OpenGroupManagerSpec: QuickSpec {
override class func spec() {
// MARK: Configuration
@TestState var testInteraction1: Interaction! = Interaction(
id: 234,
serverHash: "TestServerHash",
messageUuid: nil,
threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
authorId: "TestAuthorId",
variant: .standardOutgoing,
body: "Test",
timestampMs: 123,
receivedAtTimestampMs: 124,
wasRead: false,
hasMention: false,
expiresInSeconds: nil,
expiresStartedAtMs: nil,
linkPreviewUrl: nil,
openGroupServerMessageId: nil,
openGroupWhisperMods: false,
openGroupWhisperTo: nil
)
@TestState var testGroupThread: SessionThread! = SessionThread(
id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
variant: .community
)
@TestState var testOpenGroup: OpenGroup! = OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 10,
sequenceNumber: 5
)
@TestState var testPollInfo: OpenGroupAPI.RoomPollInfo! = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: .mockValue
)
@TestState var testMessage: OpenGroupAPI.Message! = OpenGroupAPI.Message(
id: 127,
sender: "05\(TestConstants.publicKey)",
posted: 123,
edited: nil,
deleted: nil,
seqNo: 124,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: [
"Cg0KC1Rlc3RNZXNzYWdlg",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAAAAAAA",
"AA"
].joined(),
base64EncodedSignature: nil,
reactions: nil
)
@TestState var testDirectMessage: OpenGroupAPI.DirectMessage! = OpenGroupAPI.DirectMessage(
id: 128,
sender: "15\(TestConstants.publicKey)",
recipient: "15\(TestConstants.publicKey)",
posted: 1234567890,
expires: 1234567990,
base64EncodedMessage: Data(
Bytes(arrayLiteral: 0) +
"TestMessage".bytes +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes
).base64EncodedString()
)
@TestState var mockStorage: Storage! = SynchronousStorage(
customWriter: try! DatabaseQueue(),
customMigrationTargets: [
SNUtilitiesKit.self,
SNMessagingKit.self
],
initialData: { db in
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).insert(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).insert(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.edPublicKey)!).insert(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).insert(db)
try testGroupThread.insert(db)
try testOpenGroup.insert(db)
try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db)
}
)
@TestState var mockNetwork: MockNetwork! = MockNetwork()
@TestState var mockCrypto: MockCrypto! = MockCrypto(
initialSetup: { crypto in
crypto
.when { try $0.perform(.hash(message: anyArray(), outputLength: any())) }
.thenReturn([])
crypto
.when { crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: any()
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
crypto
.when {
try $0.perform(
.sogsSignature(
message: anyArray(),
secretKey: anyArray(),
blindedSecretKey: anyArray(),
blindedPublicKey: anyArray()
)
)
}
.thenReturn("TestSogsSignature".bytes)
crypto
.when {
try $0.perform(
.signature(
message: anyArray(),
secretKey: anyArray()
)
)
}
.thenReturn("TestSignature".bytes)
crypto.when { $0.size(.nonce16) }.thenReturn(16)
crypto
.when { try $0.perform(.generateNonce16()) }
.thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes)
crypto.when { $0.size(.nonce24) }.thenReturn(24)
crypto
.when { try $0.perform(.generateNonce24()) }
.thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes)
crypto.when { $0.size(.publicKey) }.thenReturn(32)
}
)
@TestState var mockUserDefaults: MockUserDefaults! = MockUserDefaults()
@TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
initialSetup: { cache in
cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)")
}
)
@TestState var mockOGMCache: MockOGMCache! = MockOGMCache(
initialSetup: { cache in
cache.when { $0.pendingChanges }.thenReturn([])
cache.when { $0.pollers = any() }.thenReturn(())
cache.when { $0.isPolling = any() }.thenReturn(())
cache
.when { $0.defaultRoomsPublisher = any(type: [OpenGroupManager.DefaultRoomInfo].self) }
.thenReturn(())
cache
.when { $0.groupImagePublishers = any(typeA: String.self, typeB: AnyPublisher<Data, Error>.self) }
.thenReturn(())
cache
.when { $0.pendingChanges = any(type: OpenGroupAPI.PendingChange.self) }
.thenReturn(())
}
)
@TestState var mockCaches: MockCaches! = MockCaches()
.setting(cache: .general, to: mockGeneralCache)
.setting(cache: .openGroupManager, to: mockOGMCache)
@TestState var dependencies: Dependencies! = Dependencies(
storage: mockStorage,
network: mockNetwork,
crypto: mockCrypto,
standardUserDefaults: mockUserDefaults,
caches: mockCaches,
dateNow: Date(timeIntervalSince1970: 1234567890),
forceSynchronous: true
)
@TestState var disposables: [AnyCancellable]! = []
@TestState var cache: OpenGroupManager.Cache! = OpenGroupManager.Cache()
@TestState var openGroupManager: OpenGroupManager! = OpenGroupManager()
// MARK: - an OpenGroupManager
describe("an OpenGroupManager") {
afterEach {
// Just in case the shared instance had pollers created we should stop them
OpenGroupManager.shared.stopPolling()
openGroupManager.stopPolling()
}
// MARK: -- cache data
context("cache data") {
// MARK: ---- defaults the time since last open to greatestFiniteMagnitude
it("defaults the time since last open to greatestFiniteMagnitude") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(nil)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(.greatestFiniteMagnitude))
}
// MARK: ---- returns the time since the last open
it("returns the time since the last open") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567880))
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
}
// MARK: ---- caches the time since the last open
it("caches the time since the last open") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567770))
dependencies.dateNow = Date(timeIntervalSince1970: 1234567780)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567890))
// Cached value shouldn't have been updated
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
}
}
// MARK: -- when starting polling
context("when starting polling") {
beforeEach {
mockStorage.write { db in
try OpenGroup(
server: "testServer1",
roomToken: "testRoom1",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test1",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 0
).insert(db)
}
mockOGMCache.when { $0.hasPerformedInitialPoll }.thenReturn([:])
mockOGMCache.when { $0.timeSinceLastPoll }.thenReturn([:])
mockOGMCache
.when { [dependencies = dependencies!] cache in
cache.getTimeSinceLastOpen(using: dependencies)
}
.thenReturn(0)
mockOGMCache.when { $0.isPolling }.thenReturn(false)
mockOGMCache.when { $0.pollers }.thenReturn([:])
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
// MARK: ---- creates pollers for all of the open groups
it("creates pollers for all of the open groups") {
openGroupManager.startPolling(using: dependencies)
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.pollers = [
"testserver": OpenGroupAPI.Poller(for: "testserver"),
"testserver1": OpenGroupAPI.Poller(for: "testserver1")
]
})
}
// MARK: ---- updates the isPolling flag
it("updates the isPolling flag") {
openGroupManager.startPolling(using: dependencies)
2023-01-05 02:36:47 +01:00
expect(mockOGMCache)
.to(call(matchingParameters: true) { $0.isPolling = true })
}
// MARK: ---- does nothing if already polling
it("does nothing if already polling") {
mockOGMCache.when { $0.isPolling }.thenReturn(true)
openGroupManager.startPolling(using: dependencies)
expect(mockOGMCache).toNot(call { $0.pollers })
}
}
// MARK: -- when stopping polling
context("when stopping polling") {
beforeEach {
mockStorage.write { db in
try OpenGroup(
server: "testServer1",
roomToken: "testRoom1",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test1",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 0
).insert(db)
}
mockOGMCache.when { $0.isPolling }.thenReturn(true)
mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
}
// MARK: ---- removes all pollers
it("removes all pollers") {
openGroupManager.stopPolling(using: dependencies)
expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] })
}
// MARK: ---- updates the isPolling flag
it("updates the isPolling flag") {
openGroupManager.stopPolling(using: dependencies)
expect(mockOGMCache).to(call(matchingParameters: true) { $0.isPolling = false })
}
}
// MARK: -- when checking if an open group is run by session
context("when checking if an open group is run by session") {
// MARK: ---- returns false when it does not match one of Sessions servers with no scheme
it("returns false when it does not match one of Sessions servers with no scheme") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "test.test"))
.to(beFalse())
}
// MARK: ---- returns false when it does not match one of Sessions servers in http
it("returns false when it does not match one of Sessions servers in http") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://test.test"))
.to(beFalse())
}
// MARK: ---- returns false when it does not match one of Sessions servers in https
it("returns false when it does not match one of Sessions servers in https") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://test.test"))
.to(beFalse())
}
// MARK: ---- returns true when it matches Sessions SOGS IP
it("returns true when it matches Sessions SOGS IP") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS IP with http
it("returns true when it matches Sessions SOGS IP with http") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://116.203.70.33"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS IP with https
it("returns true when it matches Sessions SOGS IP with https") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://116.203.70.33"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS IP with a port
it("returns true when it matches Sessions SOGS IP with a port") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33:80"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS domain
it("returns true when it matches Sessions SOGS domain") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS domain with http
it("returns true when it matches Sessions SOGS domain with http") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://open.getsession.org"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS domain with https
it("returns true when it matches Sessions SOGS domain with https") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://open.getsession.org"))
.to(beTrue())
}
// MARK: ---- returns true when it matches Sessions SOGS domain with a port
it("returns true when it matches Sessions SOGS domain with a port") {
expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org:80"))
.to(beTrue())
}
}
// MARK: -- when checking it has an existing open group
context("when checking it has an existing open group") {
// MARK: ---- when there is a thread for the room and the cache has a poller
context("when there is a thread for the room and the cache has a poller") {
// MARK: ------ for the no-scheme variant
context("for the no-scheme variant") {
beforeEach {
mockOGMCache.when { $0.pollers }
.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
}
// MARK: -------- returns true when no scheme is provided
it("returns true when no scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a http scheme is provided
it("returns true when a http scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "http://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a https scheme is provided
it("returns true when a https scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "https://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
}
// MARK: ------ for the http variant
context("for the http variant") {
beforeEach {
mockOGMCache.when { $0.pollers }
.thenReturn(["http://testserver": OpenGroupAPI.Poller(for: "http://testserver")])
}
// MARK: -------- returns true when no scheme is provided
it("returns true when no scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a http scheme is provided
it("returns true when a http scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "http://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a https scheme is provided
it("returns true when a https scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "https://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
}
// MARK: ------ for the https variant
context("for the https variant") {
beforeEach {
mockOGMCache.when { $0.pollers }
.thenReturn(["https://testserver": OpenGroupAPI.Poller(for: "https://testserver")])
}
// MARK: -------- returns true when no scheme is provided
it("returns true when no scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a http scheme is provided
it("returns true when a http scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "http://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
// MARK: -------- returns true when a https scheme is provided
it("returns true when a https scheme is provided") {
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "https://testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
}
}
// MARK: ---- when given the legacy DNS host and there is a cached poller for the default server
context("when given the legacy DNS host and there is a cached poller for the default server") {
// MARK: ------ returns true
it("returns true") {
mockOGMCache.when { $0.pollers }
.thenReturn(["http://116.203.70.33": OpenGroupAPI.Poller(for: "http://116.203.70.33")])
mockStorage.write { db in
try SessionThread(
id: OpenGroup.idFor(roomToken: "testRoom", server: "http://116.203.70.33"),
variant: .community,
creationDateTimestamp: 0,
shouldBeVisible: true,
isPinned: false,
messageDraft: nil,
notificationSound: nil,
mutedUntilTimestamp: nil,
onlyNotifyForMentions: false
).insert(db)
}
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "http://open.getsession.org",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
}
// MARK: ---- when given the default server and there is a cached poller for the legacy DNS host
context("when given the default server and there is a cached poller for the legacy DNS host") {
// MARK: ------ returns true
it("returns true") {
mockOGMCache.when { $0.pollers }.thenReturn(["http://open.getsession.org": OpenGroupAPI.Poller(for: "http://open.getsession.org")])
mockStorage.write { db in
try SessionThread(
id: OpenGroup.idFor(roomToken: "testRoom", server: "http://open.getsession.org"),
variant: .community,
creationDateTimestamp: 0,
shouldBeVisible: true,
isPinned: false,
messageDraft: nil,
notificationSound: nil,
mutedUntilTimestamp: nil,
onlyNotifyForMentions: false
).insert(db)
}
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "http://116.203.70.33",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beTrue())
}
}
// MARK: ---- returns false when given an invalid server
it("returns false when given an invalid server") {
mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "%%%",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beFalse())
}
// MARK: ---- returns false if there is not a poller for the server in the cache
it("returns false if there is not a poller for the server in the cache") {
mockOGMCache.when { $0.pollers }.thenReturn([:])
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beFalse())
}
// MARK: ---- returns false if there is a poller for the server in the cache but no thread for the room
it("returns false if there is a poller for the server in the cache but no thread for the room") {
mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
mockStorage.write { db in
try SessionThread.deleteAll(db)
}
expect(
mockStorage.read { db -> Bool in
openGroupManager
.hasExistingOpenGroup(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
).to(beFalse())
}
}
// MARK: -- when adding
context("when adding") {
beforeEach {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
}
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomResponse)
mockOGMCache.when { $0.pollers }.thenReturn([:])
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
// MARK: ---- stores the open group server
it("stores the open group server") {
mockStorage
.writePublisher { (db: Database) -> Bool in
openGroupManager
.add(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
.flatMap { successfullyAddedGroup in
openGroupManager.performInitialRequestsAfterAdd(
successfullyAddedGroup: successfullyAddedGroup,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(
mockStorage
.read { (db: Database) in
try OpenGroup
.select(.threadId)
.asRequest(of: String.self)
.fetchOne(db)
}
)
.to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "testServer")))
}
// MARK: ---- adds a poller
it("adds a poller") {
mockStorage
.writePublisher { (db: Database) -> Bool in
openGroupManager
.add(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
.flatMap { successfullyAddedGroup in
openGroupManager.performInitialRequestsAfterAdd(
successfullyAddedGroup: successfullyAddedGroup,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")]
})
}
// MARK: ---- an existing room
context("an existing room") {
beforeEach {
mockOGMCache.when { $0.pollers }
.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
mockStorage.write { db in
try testOpenGroup.insert(db)
}
}
// MARK: ------ does not reset the sequence number or update the public key
it("does not reset the sequence number or update the public key") {
mockStorage
.writePublisher { (db: Database) -> Bool in
openGroupManager
.add(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey
.replacingOccurrences(of: "c3", with: "00")
.replacingOccurrences(of: "b3", with: "00"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
.flatMap { successfullyAddedGroup in
openGroupManager.performInitialRequestsAfterAdd(
successfullyAddedGroup: successfullyAddedGroup,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey
.replacingOccurrences(of: "c3", with: "00")
.replacingOccurrences(of: "b3", with: "00"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(
mockStorage
.read { db in
try OpenGroup
.select(.sequenceNumber)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(5))
expect(
mockStorage
.read { db in
try OpenGroup
.select(.publicKey)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal(TestConstants.publicKey))
}
}
// MARK: ---- with an invalid response
context("with an invalid response") {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.response(data: Data()))
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
// MARK: ------ fails with the error
it("fails with the error") {
var error: Error?
mockStorage
.writePublisher { (db: Database) -> Bool in
openGroupManager
.add(
db,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
.flatMap { successfullyAddedGroup in
openGroupManager.performInitialRequestsAfterAdd(
successfullyAddedGroup: successfullyAddedGroup,
roomToken: "testRoom",
server: "testServer",
publicKey: TestConstants.serverPublicKey,
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
using: dependencies
)
}
.mapError { result -> Error in error.setting(to: result) }
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(error).to(matchError(HTTPError.parsingFailed))
}
}
}
// MARK: -- when deleting
context("when deleting") {
beforeEach {
mockStorage.write { db in
try Interaction.deleteAll(db)
try SessionThread.deleteAll(db)
try testGroupThread.insert(db)
try testOpenGroup.insert(db)
try testInteraction1.insert(db)
try Interaction
.updateAll(
db,
Interaction.Columns.threadId
.set(to: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))
)
}
mockOGMCache.when { $0.pollers }.thenReturn([:])
}
// MARK: ---- removes all interactions for the thread
it("removes all interactions for the thread") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockStorage.read { db in try Interaction.fetchCount(db) })
.to(equal(0))
}
// MARK: ---- removes the given thread
it("removes the given thread") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) })
.to(equal(0))
}
// MARK: ---- and there is only one open group for this server
context("and there is only one open group for this server") {
// MARK: ------ stops the poller
it("stops the poller") {
mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] })
}
// MARK: ------ removes the open group
it("removes the open group") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockStorage.read { db in try OpenGroup.fetchCount(db) })
.to(equal(0))
}
}
// MARK: ---- and the are multiple open groups for this server
context("and the are multiple open groups for this server") {
beforeEach {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try testOpenGroup.insert(db)
try OpenGroup(
server: "testServer",
roomToken: "testRoom1",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test1",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 0,
sequenceNumber: 0,
inboxLatestMessageId: 0,
outboxLatestMessageId: 0
).insert(db)
}
}
// MARK: ------ removes the open group
it("removes the open group") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockStorage.read { db in try OpenGroup.fetchCount(db) })
.to(equal(1))
}
}
// MARK: ---- and it is the default server
context("and it is the default server") {
beforeEach {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try OpenGroup(
server: OpenGroupAPI.defaultServer,
roomToken: "testRoom",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test1",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 0,
sequenceNumber: 0,
inboxLatestMessageId: 0,
outboxLatestMessageId: 0
).insert(db)
try OpenGroup(
server: OpenGroupAPI.defaultServer,
roomToken: "testRoom1",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test1",
roomDescription: nil,
imageId: nil,
imageData: nil,
userCount: 0,
infoUpdates: 0,
sequenceNumber: 0,
inboxLatestMessageId: 0,
outboxLatestMessageId: 0
).insert(db)
}
}
// MARK: ------ does not remove the open group
it("does not remove the open group") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(mockStorage.read { db in try OpenGroup.fetchCount(db) })
.to(equal(2))
}
// MARK: ------ deactivates the open group
it("deactivates the open group") {
mockStorage.write { db in
openGroupManager
.delete(
db,
openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer),
calledFromConfigHandling: true, // Don't trigger SessionUtil logic
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
using: dependencies
)
}
expect(
mockStorage.read { db in
try OpenGroup
.select(.isActive)
.filter(id: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer))
.asRequest(of: Bool.self)
.fetchOne(db)
}
).to(beFalse())
}
}
}
// MARK: -- when handling capabilities
context("when handling capabilities") {
beforeEach {
mockStorage.write { db in
OpenGroupManager
.handleCapabilities(
db,
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []),
on: "testserver"
)
}
}
// MARK: ---- stores the capabilities
it("stores the capabilities") {
expect(mockStorage.read { db in try Capability.fetchCount(db) })
.to(equal(1))
}
}
// MARK: -- when handling room poll info
context("when handling room poll info") {
beforeEach {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try testOpenGroup.insert(db)
}
mockOGMCache.when { $0.pollers }.thenReturn([:])
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: SNUserDefaults.Date.lastOpen.rawValue)
}
.thenReturn(nil)
}
// MARK: ---- saves the updated open group
it("saves the updated open group") {
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)// { didComplete = true }
}
expect(
mockStorage.read { db in
try OpenGroup
.select(.userCount)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(10))
}
// MARK: ---- calls the completion block
it("calls the completion block") {
var didCallComplete: Bool = false
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
) { didCallComplete = true }
}
expect(didCallComplete).to(beTrue())
}
// MARK: ---- calls the room image completion block when waiting but there is no image
it("calls the room image completion block when waiting but there is no image") {
var didCallComplete: Bool = false
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
) { didCallComplete = true }
}
expect(didCallComplete).to(beTrue())
}
// MARK: ---- calls the room image completion block when waiting and there is an image
it("calls the room image completion block when waiting and there is an image") {
var didCallComplete: Bool = false
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test",
imageId: "12",
imageData: nil,
userCount: 0,
infoUpdates: 10
).insert(db)
}
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(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
) { didCallComplete = true }
}
expect(didCallComplete).to(beTrue())
}
// MARK: ---- and updating the moderator list
context("and updating the moderator list") {
// MARK: ------ successfully updates
it("successfully updates") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
moderators: ["TestMod"],
hiddenModerators: [],
admins: [],
hiddenAdmins: []
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> GroupMember? in
try GroupMember
.filter(GroupMember.Columns.groupId == OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
))
.fetchOne(db)
}
).to(equal(
GroupMember(
groupId: OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
),
profileId: "TestMod",
role: .moderator,
isHidden: false
)
))
}
// MARK: ------ updates for hidden moderators
it("updates for hidden moderators") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
moderators: [],
hiddenModerators: ["TestMod2"],
admins: [],
hiddenAdmins: []
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> GroupMember? in
try GroupMember
.filter(GroupMember.Columns.groupId == OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
))
.fetchOne(db)
}
).to(equal(
GroupMember(
groupId: OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
),
profileId: "TestMod2",
role: .moderator,
isHidden: true
)
))
}
// MARK: ------ does not insert mods if no moderators are provided
it("does not insert mods if no moderators are provided") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try GroupMember.fetchCount(db) })
.to(equal(0))
}
}
// MARK: ---- and updating the admin list
context("and updating the admin list") {
// MARK: ------ successfully updates
it("successfully updates") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
moderators: [],
hiddenModerators: [],
admins: ["TestAdmin"],
hiddenAdmins: []
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> GroupMember? in
try GroupMember
.filter(GroupMember.Columns.groupId == OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
))
.fetchOne(db)
}
).to(equal(
GroupMember(
groupId: OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
),
profileId: "TestAdmin",
role: .admin,
isHidden: false
)
))
}
// MARK: ------ updates for hidden admins
it("updates for hidden admins") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
moderators: [],
hiddenModerators: [],
admins: [],
hiddenAdmins: ["TestAdmin2"]
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> GroupMember? in
try GroupMember
.filter(GroupMember.Columns.groupId == OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
))
.fetchOne(db)
}
).to(equal(
GroupMember(
groupId: OpenGroup.idFor(
roomToken: "testRoom",
server: "testServer"
),
profileId: "TestAdmin2",
role: .admin,
isHidden: true
)
))
}
// MARK: ------ does not insert an admin if no admins are provided
it("does not insert an admin if no admins are provided") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: nil
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try GroupMember.fetchCount(db) })
.to(equal(0))
}
}
// MARK: ---- when it cannot get the open group
context("when it cannot get the open group") {
// MARK: ------ does not save the thread
it("does not save the thread") {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
}
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try OpenGroup.fetchCount(db) }).to(equal(0))
}
}
// MARK: ---- when not given a public key
context("when not given a public key") {
// MARK: ------ saves the open group with the existing public key
it("saves the open group with the existing public key") {
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: nil,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.publicKey)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal(TestConstants.publicKey))
}
}
// MARK: ---- when checking to start polling
context("when checking to start polling") {
// MARK: ------ starts a new poller when not already polling
it("starts a new poller when not already polling") {
mockOGMCache.when { $0.pollers }.thenReturn([:])
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")]
})
}
// MARK: ------ does not start a new poller when already polling
it("does not start a new poller when already polling") {
mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockOGMCache).to(call(.exactly(times: 1)) { $0.pollers })
}
}
// MARK: ---- when trying to get the room image
context("when trying to get the room image") {
beforeEach {
let image: UIImage = UIImage(color: .red, size: CGSize(width: 1, height: 1))
let imageData: Data = image.pngData()!
mockStorage.write { db in
try OpenGroup
.updateAll(db, OpenGroup.Columns.imageData.set(to: nil))
}
mockOGMCache.when { $0.groupImagePublishers }
.thenReturn([
OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher()
])
}
// MARK: ------ uses the provided room image id if available
it("uses the provided room image id if available") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
token: "test",
name: "test",
imageId: "10"
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.imageId)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal("10"))
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).toNot(beNil())
}
// MARK: ------ uses the existing room image id if none is provided
it("uses the existing room image id if none is provided") {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test",
imageId: "12",
imageData: Data([1, 2, 3]),
userCount: 0,
infoUpdates: 10
).insert(db)
}
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: nil
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.imageId)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal("12"))
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).toNot(beNil())
}
// MARK: ------ uses the new room image id if there is an existing one
it("uses the new room image id if there is an existing one") {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
try OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: TestConstants.publicKey,
isActive: true,
name: "Test",
imageId: "12",
imageData: UIImage(color: .blue, size: CGSize(width: 1, height: 1)).pngData(),
userCount: 0,
infoUpdates: 10
).insert(db)
}
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
token: "test",
name: "test",
infoUpdates: 10,
imageId: "10"
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.imageId)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal("10"))
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).toNot(beNil())
expect(mockOGMCache).to(call(.exactly(times: 1)) { $0.groupImagePublishers })
}
// MARK: ------ does nothing if there is no room image
it("does nothing if there is no room image") {
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).to(beNil())
}
// MARK: ------ does nothing if it fails to retrieve the room image
it("does nothing if it fails to retrieve the room image") {
mockOGMCache.when { $0.groupImagePublishers }
.thenReturn([
OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: HTTPError.generic).eraseToAnyPublisher()
])
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
token: "test",
name: "test",
imageId: "10"
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).to(beNil())
}
// MARK: ------ saves the retrieved room image
it("saves the retrieved room image") {
testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with(
token: "testRoom",
activeUsers: 10,
details: OpenGroupAPI.Room.mockValue.with(
token: "test",
name: "test",
infoUpdates: 10,
imageId: "10"
)
)
mockStorage.write { db in
try OpenGroupManager.handlePollInfo(
db,
pollInfo: testPollInfo,
publicKey: TestConstants.publicKey,
for: "testRoom",
on: "testServer",
waitForImageToComplete: true,
using: dependencies
)
}
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.asRequest(of: Data.self)
.fetchOne(db)
}
).toNot(beNil())
}
}
}
// MARK: -- when handling messages
context("when handling messages") {
beforeEach {
mockStorage.write { db in
try testGroupThread.insert(db)
try testOpenGroup.insert(db)
try testInteraction1.insert(db)
}
}
// MARK: ---- updates the sequence number when there are messages
it("updates the sequence number when there are messages") {
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 1,
sender: nil,
posted: 123,
edited: nil,
deleted: nil,
seqNo: 124,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: nil,
base64EncodedSignature: nil,
reactions: nil
)
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.sequenceNumber)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(124))
}
// MARK: ---- does not update the sequence number if there are no messages
it("does not update the sequence number if there are no messages") {
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.sequenceNumber)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(5))
}
// MARK: ---- ignores a message with no sender
it("ignores a message with no sender") {
mockStorage.write { db in
try Interaction.deleteAll(db)
}
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 1,
sender: nil,
posted: 123,
edited: nil,
deleted: nil,
seqNo: 124,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: Data([1, 2, 3]).base64EncodedString(),
base64EncodedSignature: nil,
reactions: nil
)
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
// MARK: ---- ignores a message with invalid data
it("ignores a message with invalid data") {
mockStorage.write { db in
try Interaction.deleteAll(db)
}
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 1,
sender: "05\(TestConstants.publicKey)",
posted: 123,
edited: nil,
deleted: nil,
seqNo: 124,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: Data([1, 2, 3]).base64EncodedString(),
base64EncodedSignature: nil,
reactions: nil
)
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
// MARK: ---- processes a message with valid data
it("processes a message with valid data") {
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [testMessage],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1))
}
// MARK: ---- processes valid messages when combined with invalid ones
it("processes valid messages when combined with invalid ones") {
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 2,
sender: "05\(TestConstants.publicKey)",
posted: 122,
edited: nil,
deleted: nil,
seqNo: 123,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: Data([1, 2, 3]).base64EncodedString(),
base64EncodedSignature: nil,
reactions: nil
),
testMessage,
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1))
}
// MARK: ---- with no data
context("with no data") {
// MARK: ------ deletes the message if we have the message
it("deletes the message if we have the message") {
mockStorage.write { db in
try Interaction
.updateAll(
db,
Interaction.Columns.openGroupServerMessageId.set(to: 127)
)
}
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 127,
sender: "05\(TestConstants.publicKey)",
posted: 123,
edited: nil,
deleted: nil,
seqNo: 123,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: nil,
base64EncodedSignature: nil,
reactions: nil
)
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
// MARK: ------ does nothing if we do not have the message
it("does nothing if we do not have the message") {
mockStorage.write { db in
OpenGroupManager.handleMessages(
db,
messages: [
OpenGroupAPI.Message(
id: 127,
sender: "05\(TestConstants.publicKey)",
posted: 123,
edited: nil,
deleted: nil,
seqNo: 123,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: nil,
base64EncodedSignature: nil,
reactions: nil
)
],
for: "testRoom",
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
}
}
// MARK: -- when handling direct messages
context("when handling direct messages") {
beforeEach {
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
using: dependencies
)
)
}
.thenReturn([])
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.generateBlindingFactor(serverPublicKey: any(), using: dependencies)
)
}
.thenReturn([])
mockCrypto
.when {
try $0.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray()
)
)
}
.thenReturn(
Data(base64Encoded:"ChQKC1Rlc3RNZXNzYWdlONCI7I/3Iw==")!.bytes +
[UInt8](repeating: 0, count: 32)
)
mockCrypto
.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }
.thenReturn(Data(hex: TestConstants.publicKey).bytes)
}
// MARK: ---- does nothing if there are no messages
it("does nothing if there are no messages") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.inboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(0))
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.outboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(0))
}
// MARK: ---- does nothing if it cannot get the open group
it("does nothing if it cannot get the open group") {
mockStorage.write { db in
try OpenGroup.deleteAll(db)
}
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.inboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(beNil())
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.outboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(beNil())
}
// MARK: ---- ignores messages with non base64 encoded data
it("ignores messages with non base64 encoded data") {
testDirectMessage = OpenGroupAPI.DirectMessage(
id: testDirectMessage.id,
sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"),
recipient: testDirectMessage.recipient,
posted: testDirectMessage.posted,
expires: testDirectMessage.expires,
base64EncodedMessage: "TestMessage%%%"
)
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
// MARK: ---- for the inbox
context("for the inbox") {
beforeEach {
mockCrypto
.when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) }
.thenReturn(Data(hex: testDirectMessage.sender.removingIdPrefixIfNeeded()).bytes)
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.verify(
.sessionId(
any(),
matchesBlindedId: any(),
serverPublicKey: any(),
using: dependencies
)
)
}
.thenReturn(false)
}
// MARK: ------ updates the inbox latest message id
it("updates the inbox latest message id") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.inboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(128))
}
// MARK: ------ ignores a message with invalid data
it("ignores a message with invalid data") {
testDirectMessage = OpenGroupAPI.DirectMessage(
id: testDirectMessage.id,
sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"),
recipient: testDirectMessage.recipient,
posted: testDirectMessage.posted,
expires: testDirectMessage.expires,
base64EncodedMessage: Data([1, 2, 3]).base64EncodedString()
)
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0))
}
// MARK: ------ processes a message with valid data
it("processes a message with valid data") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1))
}
// MARK: ------ processes valid messages when combined with invalid ones
it("processes valid messages when combined with invalid ones") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [
OpenGroupAPI.DirectMessage(
id: testDirectMessage.id,
sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"),
recipient: testDirectMessage.recipient,
posted: testDirectMessage.posted,
expires: testDirectMessage.expires,
base64EncodedMessage: Data([1, 2, 3]).base64EncodedString()
),
testDirectMessage
],
fromOutbox: false,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1))
}
}
// MARK: ---- for the outbox
context("for the outbox") {
beforeEach {
mockCrypto
.when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) }
.thenReturn(Data(hex: testDirectMessage.recipient.removingIdPrefixIfNeeded()).bytes)
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.verify(
.sessionId(
any(),
matchesBlindedId: any(),
serverPublicKey: any(),
using: dependencies
)
)
}
.thenReturn(false)
}
// MARK: ------ updates the outbox latest message id
it("updates the outbox latest message id") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(
mockStorage.read { db -> Int64? in
try OpenGroup
.select(.outboxLatestMessageId)
.asRequest(of: Int64.self)
.fetchOne(db)
}
).to(equal(128))
}
// MARK: ------ retrieves an existing blinded id lookup
it("retrieves an existing blinded id lookup") {
mockStorage.write { db in
try BlindedIdLookup(
blindedId: "15\(TestConstants.publicKey)",
sessionId: "TestSessionId",
openGroupServer: "testserver",
openGroupPublicKey: "05\(TestConstants.publicKey)"
).insert(db)
}
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try BlindedIdLookup.fetchCount(db) }).to(equal(1))
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2))
}
// MARK: ------ falls back to using the blinded id if no lookup is found
it("falls back to using the blinded id if no lookup is found") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try BlindedIdLookup.fetchCount(db) }).to(equal(1))
expect(mockStorage
.read { db -> String? in
try BlindedIdLookup
.select(.sessionId)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(beNil())
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2))
expect(
mockStorage.read { db -> SessionThread? in
try SessionThread.fetchOne(db, id: "15\(TestConstants.publicKey)")
}
).toNot(beNil())
}
// MARK: ------ ignores a message with invalid data
it("ignores a message with invalid data") {
testDirectMessage = OpenGroupAPI.DirectMessage(
id: testDirectMessage.id,
sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"),
recipient: testDirectMessage.recipient,
posted: testDirectMessage.posted,
expires: testDirectMessage.expires,
base64EncodedMessage: Data([1, 2, 3]).base64EncodedString()
)
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(1))
}
// MARK: ------ processes a message with valid data
it("processes a message with valid data") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [testDirectMessage],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2))
}
// MARK: ------ processes valid messages when combined with invalid ones
it("processes valid messages when combined with invalid ones") {
mockStorage.write { db in
OpenGroupManager.handleDirectMessages(
db,
messages: [
OpenGroupAPI.DirectMessage(
id: testDirectMessage.id,
sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"),
recipient: testDirectMessage.recipient,
posted: testDirectMessage.posted,
expires: testDirectMessage.expires,
base64EncodedMessage: Data([1, 2, 3]).base64EncodedString()
),
testDirectMessage
],
fromOutbox: true,
on: "testServer",
using: dependencies
)
}
expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2))
}
}
}
// MARK: -- when determining if a user is a moderator or an admin
context("when determining if a user is a moderator or an admin") {
beforeEach {
mockStorage.write { db in
_ = try GroupMember.deleteAll(db)
}
}
// MARK: ---- uses an empty set for moderators by default
it("uses an empty set for moderators by default") {
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ---- uses an empty set for admins by default
it("uses an empty set for admins by default") {
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ---- returns true if the key is in the moderator set
it("returns true if the key is in the moderator set") {
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(TestConstants.publicKey)",
role: .moderator,
isHidden: false
).insert(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ---- returns true if the key is in the admin set
it("returns true if the key is in the admin set") {
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(TestConstants.publicKey)",
role: .admin,
isHidden: false
).insert(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ---- returns true if the moderator is hidden
it("returns true if the moderator is hidden") {
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(TestConstants.publicKey)",
role: .moderator,
isHidden: true
).insert(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ---- returns true if the admin is hidden
it("returns true if the admin is hidden") {
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(TestConstants.publicKey)",
role: .admin,
isHidden: true
).insert(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ---- returns false if the key is not a valid session id
it("returns false if the key is not a valid session id") {
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"InvalidValue",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ---- and the key is a standard session id
context("and the key is a standard session id") {
// MARK: ------ returns false if the key is not the users session id
it("returns false if the key is not the users session id") {
mockStorage.write { db in
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns true if the key is the current users and the users unblinded id is a moderator or admin
it("returns true if the key is the current users and the users unblinded id is a moderator or admin") {
mockStorage.write { db in
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "00\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ------ returns true if the key is the current users and the users blinded id is a moderator or admin
it("returns true if the key is the current users and the users blinded id is a moderator or admin") {
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: otherKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "15\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"05\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
}
// MARK: ---- and the key is unblinded
context("and the key is unblinded") {
// MARK: ------ returns false if unable to retrieve the user ed25519 key
it("returns false if unable to retrieve the user ed25519 key") {
mockStorage.write { db in
try Identity.filter(id: .ed25519PublicKey).deleteAll(db)
try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"00\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns false if the key is not the users unblinded id
it("returns false if the key is not the users unblinded id") {
mockStorage.write { db in
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"00\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns true if the key is the current users and the users session id is a moderator or admin
it("returns true if the key is the current users and the users session id is a moderator or admin") {
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)")
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).save(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"00\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ------ returns true if the key is the current users and the users blinded id is a moderator or admin
it("returns true if the key is the current users and the users blinded id is a moderator or admin") {
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: otherKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "15\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).save(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"00\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
}
// MARK: ---- and the key is blinded
context("and the key is blinded") {
// MARK: ------ returns false if unable to retrieve the user ed25519 key
it("returns false if unable to retrieve the user ed25519 key") {
mockStorage.write { db in
try Identity.filter(id: .ed25519PublicKey).deleteAll(db)
try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"15\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns false if unable generate a blinded key
it("returns false if unable generate a blinded key") {
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(nil)
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"15\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns false if the key is not the users blinded id
it("returns false if the key is not the users blinded id") {
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: otherKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"15\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beFalse())
}
// MARK: ------ returns true if the key is the current users and the users session id is a moderator or admin
it("returns true if the key is the current users and the users session id is a moderator or admin") {
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)")
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockStorage.write { db in
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "05\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).save(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"15\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
// MARK: ------ returns true if the key is the current users and the users unblinded id is a moderator or admin
it("returns true if the key is the current users and the users unblinded id is a moderator or admin") {
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockStorage.write { db in
let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6")
try GroupMember(
groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
profileId: "00\(otherKey)",
role: .moderator,
isHidden: false
).insert(db)
try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).save(db)
try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).save(db)
}
expect(
OpenGroupManager.isUserModeratorOrAdmin(
"15\(TestConstants.publicKey)",
for: "testRoom",
on: "testServer",
using: dependencies
)
).to(beTrue())
}
}
}
// MARK: -- when getting the default rooms if needed
2022-03-17 07:12:19 +01:00
context("when getting the default rooms if needed") {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomsResponse)
2022-03-17 07:12:19 +01:00
mockStorage.write { db in
try OpenGroup.deleteAll(db)
// This is done in the 'RetrieveDefaultOpenGroupRoomsJob'
_ = try OpenGroup(
server: OpenGroupAPI.defaultServer,
roomToken: "",
publicKey: OpenGroupAPI.defaultServerPublicKey,
isActive: false,
name: "",
userCount: 0,
infoUpdates: 0
)
.insert(db)
}
mockOGMCache.when { $0.defaultRoomsPublisher }.thenReturn(nil)
mockOGMCache.when { $0.groupImagePublishers }.thenReturn([:])
mockUserDefaults.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: any())
}.thenReturn(nil)
mockUserDefaults.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.set(anyAny(), forKey: any())
}.thenReturn(())
2022-03-17 07:12:19 +01:00
}
// MARK: ---- caches the publisher if there is no cached publisher
it("caches the publisher if there is no cached publisher") {
let publisher = OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
2022-03-17 07:12:19 +01:00
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.defaultRoomsPublisher = publisher
2022-03-17 07:12:19 +01:00
})
}
// MARK: ---- returns the cached publisher if there is one
it("returns the cached publisher if there is one") {
let uniqueRoomInstance: OpenGroupAPI.Room = OpenGroupAPI.Room.mockValue.with(
token: "UniqueId",
name: ""
)
let publisher = Future<[OpenGroupManager.DefaultRoomInfo], Error> { resolver in
resolver(Result.success([(uniqueRoomInstance, nil)]))
}
.shareReplay(1)
.eraseToAnyPublisher()
mockOGMCache.when { $0.defaultRoomsPublisher }.thenReturn(publisher)
let publisher2 = OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
2022-03-17 07:12:19 +01:00
expect(publisher2.firstValue()?.map { $0.room })
.to(equal(publisher.firstValue()?.map { $0.room }))
2022-03-17 07:12:19 +01:00
}
// MARK: ---- stores the open group information
it("stores the open group information") {
2022-03-17 07:12:19 +01:00
OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
// 1 for the value returned from the API and 1 for the default added
// by the 'RetrieveDefaultOpenGroupRoomsJob' logic
expect(mockStorage.read { db -> Int in try OpenGroup.fetchCount(db) }).to(equal(2))
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.server)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal("https://open.getsession.org"))
expect(
mockStorage.read { db -> String? in
try OpenGroup
.select(.publicKey)
.asRequest(of: String.self)
.fetchOne(db)
}
).to(equal("a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"))
expect(
mockStorage.read { db -> Bool? in
try OpenGroup
.select(.isActive)
.asRequest(of: Bool.self)
.fetchOne(db)
}
).to(beFalse())
2022-03-17 07:12:19 +01:00
}
// MARK: ---- fetches rooms for the server
2022-03-17 07:12:19 +01:00
it("fetches rooms for the server") {
var response: [OpenGroupManager.DefaultRoomInfo]?
2022-03-17 07:12:19 +01:00
OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
.handleEvents(receiveOutput: { response = $0 })
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
2022-03-17 07:12:19 +01:00
expect(response?.map { $0.room })
.to(equal([OpenGroupAPI.Room.mockValue]))
2022-03-17 07:12:19 +01:00
}
// MARK: ---- will retry fetching rooms 8 times before it fails
2022-03-17 07:12:19 +01:00
it("will retry fetching rooms 8 times before it fails") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.nullResponse())
2022-03-17 07:12:19 +01:00
var error: Error?
OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
.mapError { result -> Error in error.setting(to: result) }
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
2022-03-17 07:12:19 +01:00
expect(error).to(matchError(HTTPError.parsingFailed))
expect(mockNetwork) // First attempt + 8 retries
.to(call(.exactly(times: 9)) { $0.send(.onionRequest(any(), to: any(), with: any())) })
2022-03-17 07:12:19 +01:00
}
// MARK: ---- removes the cache publisher if all retries fail
it("removes the cache publisher if all retries fail") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.nullResponse())
2022-03-17 07:12:19 +01:00
var error: Error?
OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
.mapError { result -> Error in error.setting(to: result) }
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
2022-03-17 07:12:19 +01:00
expect(error)
.to(matchError(HTTPError.parsingFailed))
2022-03-17 07:12:19 +01:00
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.defaultRoomsPublisher = nil
2022-03-17 07:12:19 +01:00
})
}
// MARK: ---- fetches the image for any rooms with images
2022-03-17 07:12:19 +01:00
it("fetches the image for any rooms with images") {
mockNetwork
.when {
$0.send(.onionRequest(
URLRequest(url: URL(string: "https://open.getsession.org/sequence")!),
to: OpenGroupAPI.defaultServer,
with: OpenGroupAPI.defaultServerPublicKey
))
}
.thenReturn(
MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
(
OpenGroupAPI.Endpoint.rooms,
[
OpenGroupAPI.Room.mockValue.with(
token: "test2",
name: "test2",
infoUpdates: 11,
imageId: "12"
)
].batchSubResponse()
)
]
)
)
mockNetwork
.when {
$0.send(
.onionRequest(
URLRequest(url: URL(string: "https://open.getsession.org/room/test2/file/12")!),
to: OpenGroupAPI.defaultServer,
with: OpenGroupAPI.defaultServerPublicKey,
timeout: FileServerAPI.fileDownloadTimeout
)
)
2022-03-17 07:12:19 +01:00
}
.thenReturn(MockNetwork.response(data: Data([1, 2, 3])))
let testDate: Date = Date(timeIntervalSince1970: 1234567890)
dependencies.dateNow = testDate
2022-03-17 07:12:19 +01:00
OpenGroupManager
.getDefaultRoomsIfNeeded(using: dependencies)
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(mockUserDefaults)
.to(call(matchingParameters: true) {
$0.set(
testDate,
forKey: SNUserDefaults.Date.lastOpenGroupImageUpdate.rawValue
)
})
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.filter(id: OpenGroup.idFor(roomToken: "test2", server: OpenGroupAPI.defaultServer))
.asRequest(of: Data.self)
.fetchOne(db)
}
).to(equal(Data([1, 2, 3])))
2022-03-17 07:12:19 +01:00
}
}
// MARK: -- when getting a room image
context("when getting a room image") {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.response(data: Data([1, 2, 3])))
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in defaults.object(forKey: any()) }
.thenReturn(nil)
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in defaults.set(anyAny(), forKey: any()) }
.thenReturn(())
mockOGMCache.when { $0.groupImagePublishers }.thenReturn([:])
mockStorage.write { db in
_ = try OpenGroup(
server: OpenGroupAPI.defaultServer,
roomToken: "testRoom",
publicKey: OpenGroupAPI.defaultServerPublicKey,
isActive: false,
name: "",
userCount: 0,
infoUpdates: 0
)
.insert(db)
}
}
// MARK: ---- retrieves the image retrieval publisher from the cache if it exists
it("retrieves the image retrieval publisher from the cache if it exists") {
let publisher = Future<Data, Error> { resolver in
resolver(Result.success(Data([5, 4, 3, 2, 1])))
}
.shareReplay(1)
.eraseToAnyPublisher()
mockOGMCache
.when { $0.groupImagePublishers }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher])
var result: Data?
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: "testServer",
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.handleEvents(receiveOutput: { result = $0 })
.sinkAndStore(in: &disposables)
expect(result).to(equal(publisher.firstValue()))
}
// MARK: ---- does not save the fetched image to storage
it("does not save the fetched image to storage") {
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: "testServer",
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.filter(id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))
.asRequest(of: Data.self)
.fetchOne(db)
}
).to(beNil())
}
// MARK: ---- does not update the image update timestamp
it("does not update the image update timestamp") {
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: "testServer",
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(mockUserDefaults)
.toNot(call(matchingParameters: true) {
$0.set(
dependencies.dateNow,
forKey: SNUserDefaults.Date.lastOpenGroupImageUpdate.rawValue
)
})
}
// MARK: ---- adds the image retrieval publisher to the cache
it("adds the image retrieval publisher to the cache") {
let publisher = OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: "testServer",
existingData: nil,
using: dependencies
)
publisher.sinkAndStore(in: &disposables)
2023-01-05 02:36:47 +01:00
expect(mockOGMCache)
.to(call(matchingParameters: true) {
$0.groupImagePublishers = [OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher]
})
}
// MARK: ---- for the default server
context("for the default server") {
// MARK: ------ fetches a new image if there is no cached one
it("fetches a new image if there is no cached one") {
var result: Data?
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: OpenGroupAPI.defaultServer,
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.handleEvents(receiveOutput: { (data: Data) in result = data })
.sinkAndStore(in: &disposables)
expect(result).to(equal(Data([1, 2, 3])))
}
// MARK: ------ saves the fetched image to storage
it("saves the fetched image to storage") {
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: OpenGroupAPI.defaultServer,
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(
mockStorage.read { db -> Data? in
try OpenGroup
.select(.imageData)
.filter(id: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer))
.asRequest(of: Data.self)
.fetchOne(db)
}
).toNot(beNil())
}
// MARK: ------ updates the image update timestamp
it("updates the image update timestamp") {
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: OpenGroupAPI.defaultServer,
existingData: nil,
using: dependencies
)
2023-01-05 02:36:47 +01:00
.sinkAndStore(in: &disposables)
expect(mockUserDefaults)
.to(call(matchingParameters: true) {
$0.set(
dependencies.dateNow,
forKey: SNUserDefaults.Date.lastOpenGroupImageUpdate.rawValue
)
})
}
// MARK: ------ and there is a cached image
context("and there is a cached image") {
beforeEach {
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: any())
}
.thenReturn(dependencies.dateNow)
mockStorage.write(updates: { db in
try OpenGroup
.filter(id: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer))
.updateAll(
db,
OpenGroup.Columns.imageData.set(to: Data([2, 3, 4]))
)
})
}
// MARK: -------- retrieves the cached image
it("retrieves the cached image") {
var result: Data?
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: OpenGroupAPI.defaultServer,
existingData: Data([2, 3, 4]),
using: dependencies
)
2023-01-05 02:36:47 +01:00
.handleEvents(receiveOutput: { (data: Data) in result = data })
.sinkAndStore(in: &disposables)
expect(result).to(equal(Data([2, 3, 4])))
}
// MARK: -------- fetches a new image if the cached on is older than a week
it("fetches a new image if the cached on is older than a week") {
let weekInSeconds: TimeInterval = (7 * 24 * 60 * 60)
let targetTimestamp: TimeInterval = (
dependencies.dateNow.timeIntervalSince1970 - weekInSeconds - 1
)
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: any())
}
.thenReturn(Date(timeIntervalSince1970: targetTimestamp))
var result: Data?
OpenGroupManager
.roomImage(
fileId: "1",
for: "testRoom",
on: OpenGroupAPI.defaultServer,
existingData: Data([2, 3, 4]),
using: dependencies
)
2023-01-05 02:36:47 +01:00
.handleEvents(receiveOutput: { (data: Data) in result = data })
.sinkAndStore(in: &disposables)
expect(result).to(equal(Data([1, 2, 3])))
}
}
}
}
}
}
}
// MARK: - Convenience Extensions
extension OpenGroupAPI.Room {
func with(
token: String? = nil,
name: String? = nil,
infoUpdates: Int64? = nil,
imageId: String? = nil,
moderators: [String]? = nil,
hiddenModerators: [String]? = nil,
admins: [String]? = nil,
hiddenAdmins: [String]? = nil
) -> OpenGroupAPI.Room {
return OpenGroupAPI.Room(
token: (token ?? self.token),
name: (name ?? self.name),
roomDescription: self.roomDescription,
infoUpdates: (infoUpdates ?? self.infoUpdates),
messageSequence: self.messageSequence,
created: self.created,
activeUsers: self.activeUsers,
activeUsersCutoff: self.activeUsersCutoff,
imageId: (imageId ?? self.imageId),
pinnedMessages: self.pinnedMessages,
admin: self.admin,
globalAdmin: self.globalAdmin,
admins: (admins ?? self.admins),
hiddenAdmins: (hiddenAdmins ?? self.hiddenAdmins),
moderator: self.moderator,
globalModerator: self.globalModerator,
moderators: (moderators ?? self.moderators),
hiddenModerators: (hiddenModerators ?? self.hiddenModerators),
read: self.read,
defaultRead: self.defaultRead,
defaultAccessible: self.defaultAccessible,
write: self.write,
defaultWrite: self.defaultWrite,
upload: self.upload,
defaultUpload: self.defaultUpload
)
}
}
extension OpenGroupAPI.RoomPollInfo {
func with(
token: String? = nil,
activeUsers: Int64? = nil,
details: OpenGroupAPI.Room? = .mockValue
) -> OpenGroupAPI.RoomPollInfo {
return OpenGroupAPI.RoomPollInfo(
token: (token ?? self.token),
activeUsers: (activeUsers ?? self.activeUsers),
admin: self.admin,
globalAdmin: self.globalAdmin,
moderator: self.moderator,
globalModerator: self.globalModerator,
read: self.read,
defaultRead: self.defaultRead,
defaultAccessible: self.defaultAccessible,
write: self.write,
defaultWrite: self.defaultWrite,
upload: self.upload,
defaultUpload: self.defaultUpload,
details: details
)
}
}
// MARK: - Mock Types
extension OpenGroupAPI.Capabilities: Mocked {
static var mockValue: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil)
}
extension OpenGroupAPI.Room: Mocked {
static var mockValue: OpenGroupAPI.Room = OpenGroupAPI.Room(
token: "test",
name: "testRoom",
roomDescription: nil,
infoUpdates: 1,
messageSequence: 1,
created: 1,
activeUsers: 1,
activeUsersCutoff: 1,
imageId: nil,
pinnedMessages: nil,
admin: false,
globalAdmin: false,
admins: [],
hiddenAdmins: nil,
moderator: false,
globalModerator: false,
moderators: [],
hiddenModerators: nil,
read: true,
defaultRead: nil,
defaultAccessible: nil,
write: true,
defaultWrite: nil,
upload: true,
defaultUpload: nil
)
}
extension OpenGroupAPI.RoomPollInfo: Mocked {
static var mockValue: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo(
token: "test",
activeUsers: 1,
admin: false,
globalAdmin: false,
moderator: false,
globalModerator: false,
read: true,
defaultRead: nil,
defaultAccessible: nil,
write: true,
defaultWrite: nil,
upload: true,
defaultUpload: false,
details: .mockValue
)
}
extension OpenGroupAPI.Message: Mocked {
static var mockValue: OpenGroupAPI.Message = OpenGroupAPI.Message(
id: 100,
sender: TestConstants.blindedPublicKey,
posted: 1,
edited: nil,
deleted: nil,
seqNo: 1,
whisper: false,
whisperMods: false,
whisperTo: nil,
base64EncodedData: nil,
base64EncodedSignature: nil,
reactions: nil
)
}
extension OpenGroupAPI.SendDirectMessageResponse: Mocked {
static var mockValue: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse(
id: 1,
sender: TestConstants.blindedPublicKey,
recipient: "testRecipient",
posted: 1122,
expires: 2233
)
}
extension OpenGroupAPI.DirectMessage: Mocked {
static var mockValue: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage(
id: 101,
sender: TestConstants.blindedPublicKey,
recipient: "testRecipient",
posted: 1212,
expires: 2323,
base64EncodedMessage: "TestMessage".data(using: .utf8)!.base64EncodedString()
)
}
extension OpenGroupAPI.BatchResponse {
static let mockUnblindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), [OpenGroupAPI.Message].mockBatchSubResponse())
]
)
static let mockBlindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), OpenGroupAPI.Message.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.inboxSince(id: 0), OpenGroupAPI.DirectMessage.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.outboxSince(id: 0), OpenGroupAPI.DirectMessage.self.mockBatchSubResponse())
]
)
static let mockCapabilitiesResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse())
]
)
static let mockRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Room.mockBatchSubResponse())
]
)
static let mockBanAndDeleteAllResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()),
(OpenGroupAPI.Endpoint.roomDeleteMessages("testRoon", sessionId: ""), NoResponse.mockBatchSubResponse())
]
)
}