From a5c565cacb853b3399f98d9d7dbff41e6da70d17 Mon Sep 17 00:00:00 2001
From: Morgan Pretty <morgan.t.pretty@gmail.com>
Date: Fri, 11 Oct 2024 12:17:39 +1100
Subject: [PATCH] Default community logic refactoring and bug fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

• Moved the default community retrieval logic to be in it's job
• Fixed a bug where a parsing failure could be incorrectly reported as a successful request
---
 Session.xcodeproj/project.pbxproj             |   6 +-
 .../Open Groups/OpenGroupSuggestionGrid.swift |   2 +-
 .../RetrieveDefaultOpenGroupRoomsJob.swift    | 129 +++-
 .../Open Groups/OpenGroupAPI.swift            |   6 +-
 .../Open Groups/OpenGroupManager.swift        | 162 +----
 ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 608 ++++++++++++++++++
 .../Open Groups/OpenGroupAPISpec.swift        |  11 +-
 .../Open Groups/OpenGroupManagerSpec.swift    | 244 ++-----
 .../Pollers/CommunityPollerSpec.swift         |   3 -
 .../_TestUtilities/MockOGMCache.swift         |   9 +-
 SessionSnodeKit/Types/BatchResponse.swift     |   3 +-
 SessionSnodeKit/Types/PreparedRequest.swift   |   3 +-
 12 files changed, 847 insertions(+), 339 deletions(-)
 create mode 100644 SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift

diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj
index 16e788239..ed745726e 100644
--- a/Session.xcodeproj/project.pbxproj
+++ b/Session.xcodeproj/project.pbxproj
@@ -680,6 +680,7 @@
 		FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; };
 		FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; };
 		FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; };
+		FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */; };
 		FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; };
 		FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; };
 		FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; };
@@ -1885,6 +1886,7 @@
 		FD481A8F2CAD16EA00ECC4CF /* LibSessionGroupInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionGroupInfoSpec.swift; sourceTree = "<group>"; };
 		FD481A912CAD17D900ECC4CF /* LibSessionGroupMembersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionGroupMembersSpec.swift; sourceTree = "<group>"; };
 		FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppContext.swift; sourceTree = "<group>"; };
+		FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJobSpec.swift; sourceTree = "<group>"; };
 		FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = "<group>"; };
 		FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = "<group>"; };
 		FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureError.swift; sourceTree = "<group>"; };
@@ -4357,8 +4359,9 @@
 		FD96F3A229DBC3BA00401309 /* Jobs */ = {
 			isa = PBXGroup;
 			children = (
-				FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */,
 				FD3FAB662AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift */,
+				FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */,
+				FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */,
 			);
 			path = Jobs;
 			sourceTree = "<group>";
@@ -6696,6 +6699,7 @@
 				FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */,
 				FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */,
 				FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */,
+				FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */,
 				FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */,
 				FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */,
 				FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */,
diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift
index cef04d866..15f4eb8af 100644
--- a/Session/Open Groups/OpenGroupSuggestionGrid.swift	
+++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift	
@@ -158,7 +158,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
         heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight)
         widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true
         
-        dependencies[singleton: .openGroupManager].getDefaultRoomsIfNeeded()
+        dependencies[cache: .openGroupManager].defaultRoomsPublisher
             .subscribe(on: DispatchQueue.global(qos: .default))
             .receive(on: DispatchQueue.main)
             .sinkUntilComplete(
diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift
index a174893e2..3a1740c54 100644
--- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift
+++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift
@@ -2,6 +2,7 @@
 
 import Foundation
 import GRDB
+import SessionSnodeKit
 import SessionUtilitiesKit
 
 // MARK: - Log.Category
@@ -25,10 +26,18 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
         deferred: @escaping (Job) -> Void,
         using dependencies: Dependencies
     ) {
-        // Don't run when inactive or not in main app
-        guard dependencies[defaults: .appGroup, key: .isMainAppActive] else {
-            return deferred(job) // Don't need to do anything if it's not the main app
-        }
+        /// Don't run when inactive or not in main app
+        ///
+        /// Additionally, since this job can be triggered by the user viewing the "Join Community" screen it's possible for multiple jobs to run at
+        /// the same time, we don't want to waste bandwidth by making redundant calls to fetch the default rooms so don't do anything if there
+        /// is already a job running
+        guard
+            dependencies[defaults: .appGroup, key: .isMainAppActive],
+            dependencies[singleton: .jobRunner]
+                .jobInfoFor(state: .running, variant: .retrieveDefaultOpenGroupRooms)
+                .filter({ key, info in key != job.id })     // Exclude this job
+                .isEmpty
+        else { return deferred(job) }
         
         // The OpenGroupAPI won't make any API calls if there is no entry for an OpenGroup
         // in the database so we need to create a dummy one to retrieve the default room data
@@ -49,10 +58,19 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
             .upserted(db)
         }
         
-        dependencies[singleton: .openGroupManager]
-            .getDefaultRoomsIfNeeded()
-            .subscribe(on: queue)
-            .receive(on: queue)
+        /// Try to retrieve the default rooms 8 times
+        dependencies[singleton: .storage]
+            .readPublisher { [dependencies] db -> Network.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
+                try OpenGroupAPI.preparedCapabilitiesAndRooms(
+                    db,
+                    on: OpenGroupAPI.defaultServer,
+                    using: dependencies
+                )
+            }
+            .flatMap { [dependencies] request in request.send(using: dependencies) }
+            .subscribe(on: queue, using: dependencies)
+            .receive(on: queue, using: dependencies)
+            .retry(8, using: dependencies)
             .sinkUntilComplete(
                 receiveCompletion: { result in
                     switch result {
@@ -64,7 +82,102 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
                             Log.error(.cat, "Failed to get default Community rooms due to error: \(error)")
                             failure(job, error, false)
                     }
+                },
+                receiveValue: { info, response in
+                    let defaultRooms: [OpenGroupManager.DefaultRoomInfo]? = dependencies[singleton: .storage].write { db -> [OpenGroupManager.DefaultRoomInfo] in
+                        // Store the capabilities first
+                        OpenGroupManager.handleCapabilities(
+                            db,
+                            capabilities: response.capabilities.data,
+                            on: OpenGroupAPI.defaultServer
+                        )
+                        
+                        let existingImageIds: [String: String] = try OpenGroup
+                            .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer)
+                            .filter(OpenGroup.Columns.imageId != nil)
+                            .fetchAll(db)
+                            .reduce(into: [:]) { result, next in result[next.id] = next.imageId }
+                        let result: [OpenGroupManager.DefaultRoomInfo] = try response.rooms.data
+                            .compactMap { room -> OpenGroupManager.DefaultRoomInfo? in
+                                /// Try to insert an inactive version of the OpenGroup (use `insert` rather than
+                                /// `save` as we want it to fail if the room already exists)
+                                do {
+                                    return (
+                                        room,
+                                        try OpenGroup(
+                                            server: OpenGroupAPI.defaultServer,
+                                            roomToken: room.token,
+                                            publicKey: OpenGroupAPI.defaultServerPublicKey,
+                                            isActive: false,
+                                            name: room.name,
+                                            roomDescription: room.roomDescription,
+                                            imageId: room.imageId,
+                                            userCount: room.activeUsers,
+                                            infoUpdates: room.infoUpdates
+                                        )
+                                        .inserted(db)
+                                    )
+                                }
+                                catch {
+                                    return try OpenGroup
+                                        .fetchOne(
+                                            db,
+                                            id: OpenGroup.idFor(
+                                                roomToken: room.token,
+                                                server: OpenGroupAPI.defaultServer
+                                            )
+                                        )
+                                        .map { (room, $0) }
+                                }
+                            }
+                        
+                        /// Schedule the room image download (if it doesn't match out current one)
+                        result.forEach { room, openGroup in
+                            let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer)
+                            
+                            guard
+                                let imageId: String = room.imageId,
+                                imageId != existingImageIds[openGroupId] ||
+                                openGroup.displayPictureFilename == nil
+                            else { return }
+                            
+                            dependencies[singleton: .jobRunner].add(
+                                db,
+                                job: Job(
+                                    variant: .displayPictureDownload,
+                                    shouldBeUnique: true,
+                                    details: DisplayPictureDownloadJob.Details(
+                                        target: .community(
+                                            imageId: imageId,
+                                            roomToken: room.token,
+                                            server: OpenGroupAPI.defaultServer
+                                        ),
+                                        timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000)
+                                    )
+                                ),
+                                canStartJob: true
+                            )
+                        }
+                        
+                        return result
+                    }
+                    
+                    /// Update the `openGroupManager` cache to have the default rooms
+                    dependencies.mutate(cache: .openGroupManager) { cache in
+                        cache.setDefaultRoomInfo(defaultRooms ?? [])
+                    }
                 }
             )
     }
+    
+    public static func run(using dependencies: Dependencies) {
+        RetrieveDefaultOpenGroupRoomsJob.run(
+            Job(variant: .retrieveDefaultOpenGroupRooms, behaviour: .runOnce),
+            queue: DispatchQueue.global(qos: .default),
+            success: { _, _ in },
+            failure: { _, _, _ in },
+            deferred: { _ in },
+            using: dependencies
+        )
+    }
 }
diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift
index d86238ead..b2fef8b8a 100644
--- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift	
+++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift	
@@ -386,12 +386,14 @@ public enum OpenGroupAPI {
                     let capabilitiesInfo: ResponseInfoType = maybeCapabilities,
                     let capabilities: Capabilities = maybeCapabilities?.body,
                     let roomsInfo: ResponseInfoType = maybeRooms,
-                    let rooms: [Room] = maybeRooms?.body
+                    let roomsResponse: Network.BatchSubResponse<[Room]> = maybeRooms,
+                    !roomsResponse.failedToParseBody
                 else { throw NetworkError.parsingFailed }
                 
+                // We might want to remove all default rooms for some reason so support that case
                 return (
                     capabilities: (info: capabilitiesInfo, data: capabilities),
-                    rooms: (info: roomsInfo, data: rooms)
+                    rooms: (info: roomsInfo, data: (roomsResponse.body ?? []))
                 )
             }
     }
diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift
index 28760df85..e3c8169c9 100644
--- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
+++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
@@ -20,7 +20,7 @@ public extension Singleton {
 public extension Cache {
     static let openGroupManager: CacheConfig<OGMCacheType, OGMImmutableCacheType> = Dependencies.create(
         identifier: "openGroupManager",
-        createInstance: { _ in OpenGroupManager.Cache() },
+        createInstance: { dependencies in OpenGroupManager.Cache(using: dependencies) },
         mutableInstance: { $0 },
         immutableInstance: { $0 }
     )
@@ -925,137 +925,40 @@ public final class OpenGroupManager {
             case .group: return false
         }
     }
-    
-    @discardableResult public func getDefaultRoomsIfNeeded() -> AnyPublisher<[DefaultRoomInfo], Error> {
-        // Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again
-        if let existingPublisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies[cache: .openGroupManager].defaultRoomsPublisher {
-            return existingPublisher
-        }
-        
-        // Try to retrieve the default rooms 8 times
-        let publisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies[singleton: .storage]
-            .readPublisher { [dependencies] db -> Network.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
-                try OpenGroupAPI.preparedCapabilitiesAndRooms(
-                    db,
-                    on: OpenGroupAPI.defaultServer,
-                    using: dependencies
-                )
-            }
-            .flatMap { [dependencies] request in request.send(using: dependencies) }
-            .subscribe(on: OpenGroupAPI.workQueue, using: dependencies)
-            .receive(on: OpenGroupAPI.workQueue, using: dependencies)
-            .retry(8, using: dependencies)
-            .map { [dependencies] info, response -> [DefaultRoomInfo]? in
-                dependencies[singleton: .storage].write { db -> [DefaultRoomInfo] in
-                    // Store the capabilities first
-                    OpenGroupManager.handleCapabilities(
-                        db,
-                        capabilities: response.capabilities.data,
-                        on: OpenGroupAPI.defaultServer
-                    )
-                    
-                    let existingImageIds: [String: String] = try OpenGroup
-                        .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer)
-                        .filter(OpenGroup.Columns.imageId != nil)
-                        .fetchAll(db)
-                        .reduce(into: [:]) { result, next in result[next.id] = next.imageId }
-                    let result: [DefaultRoomInfo] = try response.rooms.data
-                        .compactMap { room -> DefaultRoomInfo? in
-                            // Try to insert an inactive version of the OpenGroup (use 'insert'
-                            // rather than 'save' as we want it to fail if the room already exists)
-                            do {
-                                return (
-                                    room,
-                                    try OpenGroup(
-                                        server: OpenGroupAPI.defaultServer,
-                                        roomToken: room.token,
-                                        publicKey: OpenGroupAPI.defaultServerPublicKey,
-                                        isActive: false,
-                                        name: room.name,
-                                        roomDescription: room.roomDescription,
-                                        imageId: room.imageId,
-                                        userCount: room.activeUsers,
-                                        infoUpdates: room.infoUpdates
-                                    )
-                                    .inserted(db)
-                                )
-                            }
-                            catch {
-                                return try OpenGroup
-                                    .fetchOne(
-                                        db,
-                                        id: OpenGroup.idFor(
-                                            roomToken: room.token,
-                                            server: OpenGroupAPI.defaultServer
-                                        )
-                                    )
-                                    .map { (room, $0) }
-                            }
-                        }
-                    
-                    /// Schedule the room image download (if it doesn't match out current one)
-                    result.forEach { room, _ in
-                        let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer)
-                        
-                        guard
-                            let imageId: String = room.imageId,
-                            imageId != existingImageIds[openGroupId]
-                        else { return }
-                        
-                        dependencies[singleton: .jobRunner].add(
-                            db,
-                            job: Job(
-                                variant: .displayPictureDownload,
-                                shouldBeUnique: true,
-                                details: DisplayPictureDownloadJob.Details(
-                                    target: .community(
-                                        imageId: imageId,
-                                        roomToken: room.token,
-                                        server: OpenGroupAPI.defaultServer
-                                    ),
-                                    timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000)
-                                )
-                            ),
-                            canStartJob: true
-                        )
-                    }
-                    
-                    return result
-                }
-            }
-            .map { ($0 ?? []) }
-            .handleEvents(
-                receiveCompletion: { [dependencies] result in
-                    switch result {
-                        case .finished: break
-                        case .failure:
-                            dependencies.mutate(cache: .openGroupManager) { cache in
-                                cache.defaultRoomsPublisher = nil
-                            }
-                    }
-                }
-            )
-            .shareReplay(1)
-            .eraseToAnyPublisher()
-        
-        dependencies.mutate(cache: .openGroupManager) { cache in
-            cache.defaultRoomsPublisher = publisher
-        }
-        
-        // Hold on to the publisher until it has completed at least once
-        publisher.sinkUntilComplete()
-        
-        return publisher
-    }
 }
 
 // MARK: - OpenGroupManager Cache
 
 public extension OpenGroupManager {
     class Cache: OGMCacheType {
-        public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>?
+        private let dependencies: Dependencies
+        private let defaultRoomsSubject: CurrentValueSubject<[DefaultRoomInfo], Error> = CurrentValueSubject([])
+        private var _timeSinceLastOpen: TimeInterval?
+        public var pendingChanges: [OpenGroupAPI.PendingChange] = []
+        
+        public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error> {
+            defaultRoomsSubject
+                .handleEvents(
+                    receiveSubscription: { [weak defaultRoomsSubject, dependencies] _ in
+                        /// If we don't have any default rooms in memory then we haven't fetched this launch so schedule
+                        /// the `RetrieveDefaultOpenGroupRoomsJob` if one isn't already running
+                        if defaultRoomsSubject?.value.isEmpty == true {
+                            RetrieveDefaultOpenGroupRoomsJob.run(using: dependencies)
+                        }
+                    }
+                )
+                .filter { !$0.isEmpty }
+                .eraseToAnyPublisher()
+        }
+        
+        // MARK: - Initialization
+        
+        init(using dependencies: Dependencies) {
+            self.dependencies = dependencies
+        }
+        
+        // MARK: - Functions
         
-        fileprivate var _timeSinceLastOpen: TimeInterval?
         public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
             if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
                 return storedTimeSinceLastOpen
@@ -1070,7 +973,9 @@ public extension OpenGroupManager {
             return dependencies.dateNow.timeIntervalSince(lastOpen)
         }
         
-        public var pendingChanges: [OpenGroupAPI.PendingChange] = []
+        public func setDefaultRoomInfo(_ info: [DefaultRoomInfo]) {
+            defaultRoomsSubject.send(info)
+        }
     }
 }
 
@@ -1078,15 +983,16 @@ public extension OpenGroupManager {
 
 /// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way
 public protocol OGMImmutableCacheType: ImmutableCacheType {
-    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get }
+    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get }
     
     var pendingChanges: [OpenGroupAPI.PendingChange] { get }
 }
 
 public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType {
-    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get set }
+    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get }
     
     var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
     
     func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
+    func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo])
 }
diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift
new file mode 100644
index 000000000..7126ce2e9
--- /dev/null
+++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift
@@ -0,0 +1,608 @@
+// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
+
+import Foundation
+import GRDB
+
+import Quick
+import Nimble
+
+@testable import SessionSnodeKit
+@testable import SessionMessagingKit
+@testable import SessionUtilitiesKit
+
+class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec {
+    override class func spec() {
+        // MARK: Configuration
+        
+        @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in
+            dependencies.forceSynchronous = true
+            dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
+        }
+        @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage(
+            customWriter: try! DatabaseQueue(),
+            migrationTargets: [
+                SNUtilitiesKit.self,
+                SNMessagingKit.self
+            ],
+            using: dependencies,
+            initialData: { db in
+                try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db)
+                try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db)
+                try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db)
+                try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db)
+            }
+        )
+        @TestState(defaults: .appGroup, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults(
+            initialSetup: { defaults in
+                defaults.when { $0.bool(forKey: .any) }.thenReturn(true)
+            }
+        )
+        @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork(
+            initialSetup: { network in
+                network
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(
+                        MockNetwork.batchResponseData(
+                            with: [
+                                (
+                                    OpenGroupAPI.Endpoint.capabilities,
+                                    OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse()
+                                ),
+                                (
+                                    OpenGroupAPI.Endpoint.rooms,
+                                    [
+                                        OpenGroupAPI.Room.mock.with(
+                                            token: "testRoom",
+                                            name: "TestRoomName"
+                                        ),
+                                        OpenGroupAPI.Room.mock.with(
+                                            token: "testRoom2",
+                                            name: "TestRoomName2",
+                                            infoUpdates: 12,
+                                            imageId: "12"
+                                        )
+                                    ].batchSubResponse()
+                                )
+                            ]
+                        )
+                    )
+            }
+        )
+        @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner(
+            initialSetup: { jobRunner in
+                jobRunner
+                    .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }
+                    .thenReturn(nil)
+                jobRunner
+                    .when { $0.upsert(.any, job: .any, canStartJob: .any) }
+                    .thenReturn(nil)
+                jobRunner
+                    .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) }
+                    .thenReturn([:])
+            }
+        )
+        @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache(
+            initialSetup: { cache in
+                cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(())
+            }
+        )
+        @TestState var job: Job! = Job(variant: .retrieveDefaultOpenGroupRooms)
+        @TestState var error: Error? = nil
+        @TestState var permanentFailure: Bool! = false
+        @TestState var wasDeferred: Bool! = false
+        
+        // MARK: - a RetrieveDefaultOpenGroupRoomsJob
+        describe("a RetrieveDefaultOpenGroupRoomsJob") {
+            // MARK: -- defers the job if the main app is not running
+            it("defers the job if the main app is not running") {
+                mockUserDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(false)
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in  },
+                    failure: { _, _, _ in },
+                    deferred: { _ in wasDeferred = true },
+                    using: dependencies
+                )
+                
+                expect(wasDeferred).to(beTrue())
+            }
+            
+            // MARK: -- does not defer the job when the main app is running
+            it("does not defer the job when the main app is running") {
+                mockUserDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true)
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in  },
+                    failure: { _, _, _ in },
+                    deferred: { _ in wasDeferred = true },
+                    using: dependencies
+                )
+                
+                expect(wasDeferred).to(beFalse())
+            }
+            
+            // MARK: -- defers the job if there is an existing job running
+            it("defers the job if there is an existing job running") {
+                mockJobRunner
+                    .when { $0.jobInfoFor(jobs: .any, state: .running, variant: .retrieveDefaultOpenGroupRooms) }
+                    .thenReturn([
+                        101: JobRunner.JobInfo(
+                            variant: .retrieveDefaultOpenGroupRooms,
+                            threadId: nil,
+                            interactionId: nil,
+                            detailsData: nil,
+                            uniqueHashValue: nil
+                        )
+                    ])
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in  },
+                    failure: { _, _, _ in },
+                    deferred: { _ in wasDeferred = true },
+                    using: dependencies
+                )
+                
+                expect(wasDeferred).to(beTrue())
+            }
+            
+            // MARK: -- does not defer the job when there is no existing job
+            it("does not defer the job when there is no existing job") {
+                mockJobRunner
+                    .when { $0.jobInfoFor(jobs: .any, state: .running, variant: .retrieveDefaultOpenGroupRooms) }
+                    .thenReturn([:])
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in  },
+                    failure: { _, _, _ in },
+                    deferred: { _ in wasDeferred = true },
+                    using: dependencies
+                )
+                
+                expect(wasDeferred).to(beFalse())
+            }
+            
+            // MARK: -- creates an inactive entry in the database if one does not exist
+            it("creates an inactive entry in the database if one does not exist") {
+                mockNetwork
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(MockNetwork.errorResponse())
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) }
+                expect(openGroups?.count).to(equal(1))
+                expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer]))
+                expect(openGroups?.map { $0.roomToken }).to(equal([""]))
+                expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey]))
+                expect(openGroups?.map { $0.isActive }).to(equal([false]))
+                expect(openGroups?.map { $0.name }).to(equal([""]))
+            }
+            
+            // MARK: -- does not create a new entry if one already exists
+            it("does not create a new entry if one already exists") {
+                mockNetwork
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(MockNetwork.errorResponse())
+                
+                mockStorage.write { db in
+                    try OpenGroup(
+                        server: OpenGroupAPI.defaultServer,
+                        roomToken: "",
+                        publicKey: OpenGroupAPI.defaultServerPublicKey,
+                        isActive: false,
+                        name: "TestExisting",
+                        userCount: 0,
+                        infoUpdates: 0
+                    )
+                    .insert(db)
+                }
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) }
+                expect(openGroups?.count).to(equal(1))
+                expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer]))
+                expect(openGroups?.map { $0.roomToken }).to(equal([""]))
+                expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey]))
+                expect(openGroups?.map { $0.isActive }).to(equal([false]))
+                expect(openGroups?.map { $0.name }).to(equal(["TestExisting"]))
+            }
+            
+            // MARK: -- sends the correct request
+            it("sends the correct request") {
+                mockStorage.write { db in
+                    try OpenGroup(
+                        server: OpenGroupAPI.defaultServer,
+                        roomToken: "",
+                        publicKey: OpenGroupAPI.defaultServerPublicKey,
+                        isActive: false,
+                        name: "TestExisting",
+                        userCount: 0,
+                        infoUpdates: 0
+                    )
+                    .insert(db)
+                }
+                let expectedRequest: Network.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse>! = mockStorage.read { db in
+                    try OpenGroupAPI.preparedCapabilitiesAndRooms(
+                        db,
+                        on: OpenGroupAPI.defaultServer,
+                        using: dependencies
+                    )
+                }
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockNetwork)
+                    .to(call { network in
+                        network.send(
+                            expectedRequest.body,
+                            to: expectedRequest.destination,
+                            requestTimeout: expectedRequest.requestTimeout,
+                            requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout
+                        )
+                    })
+            }
+            
+            // MARK: -- will retry 8 times before it fails
+            it("will retry 8 times before it fails") {
+                mockNetwork
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(MockNetwork.nullResponse())
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, error_, permanentFailure_ in
+                        error = error_
+                        permanentFailure = permanentFailure_
+                    },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(error).to(matchError(NetworkError.parsingFailed))
+                expect(mockNetwork)   // First attempt + 8 retries
+                    .to(call(.exactly(times: 9)) { network in
+                        network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any)
+                    })
+            }
+            
+            // MARK: -- stores the updated capabilities
+            it("stores the updated capabilities") {
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                let capabilities: [Capability]? = mockStorage.read { db in try Capability.fetchAll(db) }
+                expect(capabilities?.count).to(equal(2))
+                expect(capabilities?.map { $0.openGroupServer })
+                    .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer]))
+                expect(capabilities?.map { $0.variant }).to(equal([.blind, .reactions]))
+                expect(capabilities?.map { $0.isMissing }).to(equal([false, false]))
+            }
+            
+            // MARK: -- inserts the returned rooms
+            it("inserts the returned rooms") {
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) }
+                expect(openGroups?.count).to(equal(3))  // 1 for the entry used to fetch the default rooms
+                expect(openGroups?.map { $0.server })
+                    .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer]))
+                expect(openGroups?.map { $0.roomToken }).to(equal(["", "testRoom", "testRoom2"]))
+                expect(openGroups?.map { $0.publicKey })
+                    .to(equal([
+                        OpenGroupAPI.defaultServerPublicKey,
+                        OpenGroupAPI.defaultServerPublicKey,
+                        OpenGroupAPI.defaultServerPublicKey
+                    ]))
+                expect(openGroups?.map { $0.isActive }).to(equal([false, false, false]))
+                expect(openGroups?.map { $0.name }).to(equal(["", "TestRoomName", "TestRoomName2"]))
+            }
+            
+            // MARK: -- does not override existing rooms that were returned
+            it("does not override existing rooms that were returned") {
+                mockStorage.write { db in
+                    try OpenGroup(
+                        server: OpenGroupAPI.defaultServer,
+                        roomToken: "testRoom",
+                        publicKey: OpenGroupAPI.defaultServerPublicKey,
+                        isActive: false,
+                        name: "TestExisting",
+                        userCount: 0,
+                        infoUpdates: 0
+                    )
+                    .insert(db)
+                }
+                mockNetwork
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(
+                        MockNetwork.batchResponseData(
+                            with: [
+                                (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
+                                (
+                                    OpenGroupAPI.Endpoint.rooms,
+                                    try! JSONEncoder().with(outputFormatting: .sortedKeys).encode(
+                                        Network.BatchSubResponse(
+                                            code: 200,
+                                            headers: [:],
+                                            body: [
+                                                OpenGroupAPI.Room.mock.with(
+                                                    token: "testRoom",
+                                                    name: "TestReplacementName"
+                                                )
+                                            ],
+                                            failedToParseBody: false
+                                        )
+                                    )
+                                )
+                            ]
+                        )
+                    )
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) }
+                expect(openGroups?.count).to(equal(2))  // 1 for the entry used to fetch the default rooms
+                expect(openGroups?.map { $0.server })
+                    .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer]))
+                expect(openGroups?.map { $0.roomToken }.sorted()).to(equal(["", "testRoom"]))
+                expect(openGroups?.map { $0.publicKey })
+                    .to(equal([OpenGroupAPI.defaultServerPublicKey, OpenGroupAPI.defaultServerPublicKey]))
+                expect(openGroups?.map { $0.isActive }).to(equal([false, false]))
+                expect(openGroups?.map { $0.name }.sorted()).to(equal(["", "TestExisting"]))
+            }
+            
+            // MARK: -- schedules a display picture download
+            it("schedules a display picture download") {
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockJobRunner)
+                    .to(call(matchingParameters: .all) {
+                        $0.add(
+                            .any,
+                            job: Job(
+                                variant: .displayPictureDownload,
+                                shouldBeUnique: true,
+                                details: DisplayPictureDownloadJob.Details(
+                                    target: .community(
+                                        imageId: "12",
+                                        roomToken: "testRoom2",
+                                        server: OpenGroupAPI.defaultServer
+                                    ),
+                                    timestamp: 1234567890
+                                )
+                            ),
+                            dependantJob: nil,
+                            canStartJob: true
+                        )
+                    })
+            }
+            
+            // MARK: -- schedules a display picture download if the imageId has changed
+            it("schedules a display picture download if the imageId has changed") {
+                mockStorage.write { db in
+                    try OpenGroup(
+                        server: OpenGroupAPI.defaultServer,
+                        roomToken: "testRoom2",
+                        publicKey: OpenGroupAPI.defaultServerPublicKey,
+                        isActive: false,
+                        name: "TestExisting",
+                        imageId: "10",
+                        userCount: 0,
+                        infoUpdates: 10,
+                        displayPictureFilename: "TestFilename"
+                    )
+                    .insert(db)
+                }
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockJobRunner)
+                    .to(call(matchingParameters: .all) {
+                        $0.add(
+                            .any,
+                            job: Job(
+                                variant: .displayPictureDownload,
+                                shouldBeUnique: true,
+                                details: DisplayPictureDownloadJob.Details(
+                                    target: .community(
+                                        imageId: "12",
+                                        roomToken: "testRoom2",
+                                        server: OpenGroupAPI.defaultServer
+                                    ),
+                                    timestamp: 1234567890
+                                )
+                            ),
+                            dependantJob: nil,
+                            canStartJob: true
+                        )
+                    })
+            }
+            
+            // MARK: -- does not schedule a display picture download if there is no imageId
+            it("does not schedule a display picture download if there is no imageId") {
+                mockNetwork
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(
+                        MockNetwork.batchResponseData(
+                            with: [
+                                (
+                                    OpenGroupAPI.Endpoint.capabilities,
+                                    OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse()
+                                ),
+                                (
+                                    OpenGroupAPI.Endpoint.rooms,
+                                    [
+                                        OpenGroupAPI.Room.mock.with(
+                                            token: "testRoom",
+                                            name: "TestRoomName"
+                                        ),
+                                        OpenGroupAPI.Room.mock.with(
+                                            token: "testRoom2",
+                                            name: "TestRoomName2"
+                                        )
+                                    ].batchSubResponse()
+                                )
+                            ]
+                        )
+                    )
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockJobRunner)
+                    .toNot(call { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) })
+            }
+            
+            // MARK: -- does not schedule a display picture download if the imageId matches and the image has already been downloaded
+            it("does not schedule a display picture download if the imageId matches and the image has already been downloaded") {
+                mockStorage.write { db in
+                    try OpenGroup(
+                        server: OpenGroupAPI.defaultServer,
+                        roomToken: "testRoom2",
+                        publicKey: OpenGroupAPI.defaultServerPublicKey,
+                        isActive: false,
+                        name: "TestExisting",
+                        imageId: "12",
+                        userCount: 0,
+                        infoUpdates: 12,
+                        displayPictureFilename: "TestFilename"
+                    )
+                    .insert(db)
+                }
+                
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockJobRunner)
+                    .toNot(call { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) })
+            }
+            
+            // MARK: -- updates the cache with the default rooms
+            it("does not schedule a display picture download if the imageId matches and the image has already been downloaded") {
+                RetrieveDefaultOpenGroupRoomsJob.run(
+                    job,
+                    queue: .main,
+                    success: { _, _ in },
+                    failure: { _, _, _ in },
+                    deferred: { _ in },
+                    using: dependencies
+                )
+                
+                expect(mockOGMCache)
+                    .toNot(call(matchingParameters: .all) {
+                        $0.setDefaultRoomInfo([
+                            (
+                                room: OpenGroupAPI.Room.mock.with(
+                                    token: "testRoom",
+                                    name: "TestRoomName"
+                                ),
+                                openGroup: OpenGroup(
+                                    server: OpenGroupAPI.defaultServer,
+                                    roomToken: "testRoom",
+                                    publicKey: OpenGroupAPI.defaultServerPublicKey,
+                                    isActive: false,
+                                    name: "TestRoomName",
+                                    userCount: 0,
+                                    infoUpdates: 0
+                                )
+                            ),
+                            (
+                                room: OpenGroupAPI.Room.mock.with(
+                                    token: "testRoom2",
+                                    name: "TestRoomName2",
+                                    infoUpdates: 12,
+                                    imageId: "12"
+                                ),
+                                openGroup: OpenGroup(
+                                    server: OpenGroupAPI.defaultServer,
+                                    roomToken: "testRoom2",
+                                    publicKey: OpenGroupAPI.defaultServerPublicKey,
+                                    isActive: false,
+                                    name: "TestRoomName2",
+                                    imageId: "12",
+                                    userCount: 0,
+                                    infoUpdates: 12,
+                                    displayPictureFilename: nil
+                                )
+                            )
+                        ])
+                    })
+            }
+        }
+    }
+}
diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift
index aab6d09dd..595dbcde2 100644
--- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift	
@@ -567,13 +567,20 @@ class OpenGroupAPISpec: QuickSpec {
                 }
                 
                 // MARK: ---- and given an invalid response
-                
                 context("and given an invalid response") {
                     // MARK: ------ errors when not given a room response
                     it("errors when not given a room response") {
                         mockNetwork
                             .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
-                            .thenReturn(Network.BatchResponse.mockCapabilitiesAndBanResponse)
+                            .thenReturn(
+                                MockNetwork.batchResponseData(with: [
+                                    (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
+                                    (
+                                        OpenGroupAPI.Endpoint.userBan(""),
+                                        OpenGroupAPI.DirectMessage.mockBatchSubResponse()
+                                    )
+                                ])
+                            )
                         
                         var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)?
                         
diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift
index 1e12894ba..44a833c78 100644
--- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
@@ -131,7 +131,13 @@ class OpenGroupManagerSpec: QuickSpec {
                     .thenReturn([:])
             }
         )
-        @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork()
+        @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork(
+            initialSetup: { network in
+                network
+                    .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
+                    .thenReturn(MockNetwork.errorResponse())
+            }
+        )
         @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto(
             initialSetup: { crypto in
                 crypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn([])
@@ -170,6 +176,11 @@ class OpenGroupManagerSpec: QuickSpec {
                 defaults.when { $0.integer(forKey: .any) }.thenReturn(0)
             }
         )
+        @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults(
+            initialSetup: { defaults in
+                defaults.when { $0.bool(forKey: .any) }.thenReturn(false)
+            }
+        )
         @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
             initialSetup: { cache in
                 cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey))
@@ -177,12 +188,10 @@ class OpenGroupManagerSpec: QuickSpec {
         )
         @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache(
             initialSetup: { cache in
-                cache
-                    .when { $0.defaultRoomsPublisher = .any(type: [OpenGroupManager.DefaultRoomInfo].self) }
-                    .thenReturn(())
                 cache.when { $0.pendingChanges }.thenReturn([])
                 cache.when { $0.pendingChanges = .any }.thenReturn(())
                 cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0)
+                cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(())
             }
         )
         @TestState var mockPoller: MockCommunityPoller! = MockCommunityPoller(
@@ -202,7 +211,7 @@ class OpenGroupManagerSpec: QuickSpec {
         )
         @TestState var disposables: [AnyCancellable]! = []
         
-        @TestState var cache: OpenGroupManager.Cache! = OpenGroupManager.Cache()
+        @TestState var cache: OpenGroupManager.Cache! = OpenGroupManager.Cache(using: dependencies)
         @TestState var openGroupManager: OpenGroupManager! = OpenGroupManager(using: dependencies)
         
         // MARK: - an OpenGroupManager
@@ -2738,206 +2747,51 @@ class OpenGroupManagerSpec: QuickSpec {
                 }
             }
             
-            // MARK: -- when getting the default rooms if needed
-            context("when getting the default rooms if needed") {
-                beforeEach {
-                    mockNetwork
-                        .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
-                        .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomsResponse)
-                    
+            // MARK: -- when accessing the default rooms publisher
+            context("when accessing the default rooms publisher") {
+                // MARK: ---- starts a job to retrieve the default rooms if we have none
+                it("starts a job to retrieve the default rooms if we have none") {
+                    mockAppGroupDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true)
                     mockStorage.write { db in
-                        try OpenGroup.deleteAll(db)
-                        
-                        // This is done in the 'RetrieveDefaultOpenGroupRoomsJob'
-                        _ = try OpenGroup(
+                        try OpenGroup(
                             server: OpenGroupAPI.defaultServer,
                             roomToken: "",
                             publicKey: OpenGroupAPI.defaultServerPublicKey,
                             isActive: false,
-                            name: "",
+                            name: "TestExisting",
                             userCount: 0,
                             infoUpdates: 0
                         )
                         .insert(db)
                     }
-                    
-                    mockOGMCache.when { $0.defaultRoomsPublisher }.thenReturn(nil)
-                    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(())
-                }
-                
-                // MARK: ---- caches the publisher if there is no cached publisher
-                it("caches the publisher if there is no cached publisher") {
-                    let publisher = openGroupManager.getDefaultRoomsIfNeeded()
-                    
-                    expect(mockOGMCache)
-                        .to(call(matchingParameters: .all) {
-                            $0.defaultRoomsPublisher = publisher
-                        })
-                }
-                
-                // MARK: ---- returns the cached publisher if there is one
-                it("returns the cached publisher if there is one") {
-                    let uniqueRoomInstance: OpenGroupAPI.Room = OpenGroupAPI.Room.mock.with(
-                        token: "UniqueId",
-                        name: ""
-                    )
-                    let group: OpenGroup = OpenGroup(
-                        server: "testServer",
-                        roomToken: "UniqueId",
-                        publicKey: "",
-                        isActive: true,
-                        name: "",
-                        userCount: 0,
-                        infoUpdates: 0
-                    )
-                    let publisher = Future<[OpenGroupManager.DefaultRoomInfo], Error> { resolver in
-                        resolver(Result.success([(uniqueRoomInstance, group)]))
+                    let expectedRequest: Network.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse>! = mockStorage.read { db in
+                        try OpenGroupAPI.preparedCapabilitiesAndRooms(
+                            db,
+                            on: OpenGroupAPI.defaultServer,
+                            using: dependencies
+                        )
                     }
-                    .shareReplay(1)
-                    .eraseToAnyPublisher()
-                    mockOGMCache.when { $0.defaultRoomsPublisher }.thenReturn(publisher)
-                    let publisher2 = openGroupManager.getDefaultRoomsIfNeeded()
-                    
-                    expect(publisher2.firstValue()?.map { $0.room })
-                        .to(equal(publisher.firstValue()?.map { $0.room }))
-                }
-                
-                // MARK: ---- stores the open group information
-                it("stores the open group information") {
-                    openGroupManager.getDefaultRoomsIfNeeded()
-                    
-                    // 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())
-                }
-                
-                // MARK: ---- fetches rooms for the server
-                it("fetches rooms for the server") {
-                    var response: [OpenGroupManager.DefaultRoomInfo]?
-                    
-                    openGroupManager.getDefaultRoomsIfNeeded()
-                        .handleEvents(receiveOutput: { response = $0 })
-                        .sinkAndStore(in: &disposables)
-                    
-                    expect(response?.map { $0.room })
-                        .to(equal([OpenGroupAPI.Room.mock]))
-                }
-                
-                // MARK: ---- will retry fetching rooms 8 times before it fails
-                it("will retry fetching rooms 8 times before it fails") {
-                    mockNetwork
-                        .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
-                        .thenReturn(MockNetwork.nullResponse())
-                    
-                    var error: Error?
-                    
-                    openGroupManager.getDefaultRoomsIfNeeded()
-                        .mapError { result -> Error in error.setting(to: result) }
-                        .sinkAndStore(in: &disposables)
+                    cache.defaultRoomsPublisher.sinkUntilComplete()
                     
-                    expect(error).to(matchError(NetworkError.parsingFailed))
-                    expect(mockNetwork)   // First attempt + 8 retries
-                        .to(call(.exactly(times: 9)) { network in
-                            network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any)
+                    expect(mockNetwork)
+                        .to(call { network in
+                            network.send(
+                                expectedRequest.body,
+                                to: expectedRequest.destination,
+                                requestTimeout: expectedRequest.requestTimeout,
+                                requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout
+                            )
                         })
                 }
                 
-                // MARK: ---- removes the cache publisher if all retries fail
-                it("removes the cache publisher if all retries fail") {
-                    mockNetwork
-                        .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
-                        .thenReturn(MockNetwork.nullResponse())
-                    
-                    var error: Error?
-                    
-                    openGroupManager.getDefaultRoomsIfNeeded()
-                        .mapError { result -> Error in error.setting(to: result) }
-                        .sinkAndStore(in: &disposables)
+                // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms
+                it("does not start a job to retrieve the default rooms if we already have rooms") {
+                    mockAppGroupDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true)
+                    cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)])
+                    cache.defaultRoomsPublisher.sinkUntilComplete()
                     
-                    expect(error)
-                        .to(matchError(NetworkError.parsingFailed))
-                    expect(mockOGMCache)
-                        .to(call(matchingParameters: .all) {
-                            $0.defaultRoomsPublisher = nil
-                        })
-                }
-                
-                // MARK: ---- schedules jobs to download any room images
-                it("schedules jobs to download any room images") {
-                    mockNetwork
-                        .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
-                        .thenReturn(
-                            MockNetwork.batchResponseData(
-                                with: [
-                                    (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),
-                                    (
-                                        OpenGroupAPI.Endpoint.rooms,
-                                        [
-                                            OpenGroupAPI.Room.mock.with(
-                                                token: "test2",
-                                                name: "test2",
-                                                infoUpdates: 11,
-                                                imageId: "12"
-                                            )
-                                        ].batchSubResponse()
-                                    )
-                                ]
-                            )
-                        )
-                    
-                    openGroupManager
-                        .getDefaultRoomsIfNeeded()
-                        .sinkAndStore(in: &disposables)
-                    
-                    expect(mockJobRunner)
-                        .to(call(matchingParameters: .all) {
-                            $0.add(
-                                .any,
-                                job: Job(
-                                    variant: .displayPictureDownload,
-                                    shouldBeUnique: true,
-                                    details: DisplayPictureDownloadJob.Details(
-                                        target: .community(
-                                            imageId: "12",
-                                            roomToken: "test2",
-                                            server: OpenGroupAPI.defaultServer
-                                        ),
-                                        timestamp: 1234567890
-                                    )
-                                ),
-                                dependantJob: nil,
-                                canStartJob: true
-                            )
-                        })
+                    expect(mockNetwork)
+                        .toNot(call { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) })
                 }
             }
         }
@@ -3014,6 +2868,18 @@ extension OpenGroupAPI.RoomPollInfo {
 
 // MARK: - Mock Types
 
+extension OpenGroup: Mocked {
+    static var mock: OpenGroup = OpenGroup(
+        server: "testserver",
+        roomToken: "testRoom",
+        publicKey: TestConstants.serverPublicKey,
+        isActive: true,
+        name: "testRoom",
+        userCount: 0,
+        infoUpdates: 0
+    )
+}
+
 extension OpenGroupAPI.Capabilities: Mocked {
     static var mock: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil)
 }
diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift
index 2dea6c6be..d66a07ad8 100644
--- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift	
+++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift	
@@ -77,9 +77,6 @@ class CommunityPollerSpec: QuickSpec {
         )
         @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache(
             initialSetup: { cache in
-                cache
-                    .when { $0.defaultRoomsPublisher = .any(type: [OpenGroupManager.DefaultRoomInfo].self) }
-                    .thenReturn(())
                 cache.when { $0.pendingChanges }.thenReturn([])
                 cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0)
             }
diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
index 657a2fdda..103b644ad 100644
--- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
+++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
@@ -7,9 +7,8 @@ import SessionUtilitiesKit
 @testable import SessionMessagingKit
 
 class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
-    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? {
-        get { return mock() }
-        set { mockNoReturn(args: [newValue]) }
+    var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> {
+        mock()
     }
     
     var pendingChanges: [OpenGroupAPI.PendingChange] {
@@ -20,4 +19,8 @@ class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
     func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
         return mock(args: [dependencies])
     }
+    
+    func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo]) {
+        mockNoReturn(args: [info])
+    }
 }
diff --git a/SessionSnodeKit/Types/BatchResponse.swift b/SessionSnodeKit/Types/BatchResponse.swift
index a3f3a2da7..5e5f8eac6 100644
--- a/SessionSnodeKit/Types/BatchResponse.swift
+++ b/SessionSnodeKit/Types/BatchResponse.swift
@@ -107,7 +107,7 @@ public extension Network {
             code: Int,
             headers: [String: String] = [:],
             body: T? = nil,
-            failedToParseBody: Bool = false
+            failedToParseBody: Bool
         ) {
             self.code = code
             self.headers = headers
@@ -151,6 +151,7 @@ extension Network.BatchSubResponse: Decodable {
 
 protocol ErasedBatchSubResponse: ResponseInfoType {
     var erasedBody: Any? { get }
+    var failedToParseBody: Bool { get }
 }
 
 // MARK: - Convenience
diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionSnodeKit/Types/PreparedRequest.swift
index 027d81859..c6cf77171 100644
--- a/SessionSnodeKit/Types/PreparedRequest.swift
+++ b/SessionSnodeKit/Types/PreparedRequest.swift
@@ -362,7 +362,8 @@ extension Network.PreparedRequest: ErasedPreparedRequest {
                     return Network.BatchSubResponse(
                         code: subResponse.code,
                         headers: subResponse.headers,
-                        body: try originalType.from(subResponse.erasedBody).map { try converter(info, $0) }
+                        body: try originalType.from(subResponse.erasedBody).map { try converter(info, $0) },
+                        failedToParseBody: subResponse.failedToParseBody
                     )
                     
                 default: return try originalType.from(data).map { try converter(info, $0) } as Any