901 lines
32 KiB
Swift
901 lines
32 KiB
Swift
import Foundation
|
|
import HAKit
|
|
import PromiseKit
|
|
import RealmSwift
|
|
@testable import Shared
|
|
import XCTest
|
|
|
|
class ModelManagerTests: XCTestCase {
|
|
private var realm: Realm!
|
|
private var testQueue: DispatchQueue!
|
|
private var manager: ModelManager!
|
|
private var servers: FakeServerManager!
|
|
private var api1: FakeHomeAssistantAPI!
|
|
private var api2: FakeHomeAssistantAPI!
|
|
private var apiConnection1: HAMockConnection!
|
|
private var apiConnection2: HAMockConnection!
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
|
|
testQueue = DispatchQueue(label: #file)
|
|
manager = ModelManager()
|
|
manager.workQueue = testQueue
|
|
|
|
servers = FakeServerManager(initial: 0)
|
|
let server1 = servers.add(identifier: "s1", serverInfo: .fake())
|
|
let server2 = servers.add(identifier: "s2", serverInfo: .fake())
|
|
api1 = FakeHomeAssistantAPI(server: server1)
|
|
api2 = FakeHomeAssistantAPI(server: server2)
|
|
apiConnection1 = HAMockConnection()
|
|
api1.connection = apiConnection1
|
|
apiConnection2 = HAMockConnection()
|
|
api2.connection = apiConnection2
|
|
Current.servers = servers
|
|
Current.cachedApis = [server1.identifier: api1, server2.identifier: api2]
|
|
|
|
let executionIdentifier = UUID().uuidString
|
|
try testQueue.sync {
|
|
realm = try Realm(configuration: .init(inMemoryIdentifier: executionIdentifier), queue: testQueue)
|
|
Current.realm = { self.realm }
|
|
}
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
Current.realm = Realm.live
|
|
TestStoreModel1.lastDidUpdates = []
|
|
TestStoreModel1.lastWillDeleteIds = []
|
|
TestStoreModel1.updateFalseIds = []
|
|
|
|
TestStoreModel3.lastWillDeleteIds = []
|
|
}
|
|
|
|
func testObserve() throws {
|
|
try testQueue.sync {
|
|
let results = AnyRealmCollection(realm.objects(TestStoreModel1.self))
|
|
|
|
let executedExpectation = self.expectation(description: "observed")
|
|
executedExpectation.expectedFulfillmentCount = 2
|
|
|
|
var didObserveCount = 0
|
|
|
|
manager.observe(for: results) { collection -> Promise<Void> in
|
|
XCTAssertEqual(Array(collection), Array(results))
|
|
didObserveCount += 1
|
|
executedExpectation.fulfill()
|
|
return .value(())
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 0)
|
|
|
|
try realm.write {
|
|
realm.add(with(TestStoreModel1()) {
|
|
$0.identifier = "123"
|
|
$0.value = "456"
|
|
})
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 1)
|
|
|
|
try realm.write {
|
|
realm.add(with(TestStoreModel1()) {
|
|
$0.identifier = "qrs"
|
|
$0.value = "tuv"
|
|
})
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 2)
|
|
|
|
wait(for: [executedExpectation], timeout: 10.0)
|
|
}
|
|
}
|
|
|
|
func testCleanupWithoutItems() {
|
|
let promise = manager.cleanup(definitions: [])
|
|
XCTAssertNoThrow(try hang(promise))
|
|
}
|
|
|
|
func testNoneToCleanUp() throws {
|
|
let now = Date()
|
|
Current.date = { now }
|
|
|
|
let models = [
|
|
TestDeleteModel1(now.addingTimeInterval(-1)),
|
|
TestDeleteModel1(now.addingTimeInterval(-2)),
|
|
TestDeleteModel1(now.addingTimeInterval(-3)),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(models)
|
|
}
|
|
}
|
|
|
|
XCTAssertTrue(models.allSatisfy { $0.realm != nil })
|
|
|
|
let promise = manager.cleanup(
|
|
definitions: [
|
|
.init(
|
|
model: TestDeleteModel1.self,
|
|
createdKey: #keyPath(TestDeleteModel1.createdAt),
|
|
duration: .init(value: 100, unit: .seconds)
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertNoThrow(try hang(promise))
|
|
XCTAssertTrue(models.allSatisfy { !$0.isInvalidated })
|
|
}
|
|
|
|
func testCleanupRemovesOnlyOlder() throws {
|
|
let now = Date()
|
|
|
|
let deletedTimeInterval1: TimeInterval = 100
|
|
let deletedTimeInterval2: TimeInterval = 1000
|
|
|
|
let deletedLimit1 = Date(timeIntervalSinceNow: -deletedTimeInterval1)
|
|
let deletedLimit2 = Date(timeIntervalSinceNow: -deletedTimeInterval2)
|
|
|
|
Current.date = { now }
|
|
|
|
let (expectedExpired, expectedAlive) = try testQueue.sync { () -> (expired: [Object], alive: [Object]) in
|
|
let expired = [
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-1)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-100)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-1000)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-1)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-100)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-1000)),
|
|
]
|
|
|
|
let alive = [
|
|
// shouldn't be deleted due to time
|
|
TestDeleteModel1(deletedLimit1),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(10)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(100)),
|
|
TestDeleteModel2(deletedLimit2),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(10)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(100)),
|
|
// shouldn't be deleted due to not being requested
|
|
TestDeleteModel3(now.addingTimeInterval(-10000)),
|
|
]
|
|
|
|
try realm.write {
|
|
realm.add(expired)
|
|
realm.add(alive)
|
|
}
|
|
|
|
return (expired, alive)
|
|
}
|
|
|
|
XCTAssertTrue(expectedExpired.allSatisfy { $0.realm != nil })
|
|
XCTAssertTrue(expectedAlive.allSatisfy { $0.realm != nil })
|
|
|
|
let promise = manager.cleanup(
|
|
definitions: [
|
|
.init(
|
|
model: TestDeleteModel1.self,
|
|
createdKey: #keyPath(TestDeleteModel1.createdAt),
|
|
duration: .init(value: deletedTimeInterval1, unit: .seconds)
|
|
),
|
|
.init(
|
|
model: TestDeleteModel2.self,
|
|
createdKey: #keyPath(TestDeleteModel2.createdAt),
|
|
duration: .init(value: deletedTimeInterval2, unit: .seconds)
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertNoThrow(try hang(promise))
|
|
XCTAssertTrue(expectedExpired.allSatisfy(\.isInvalidated))
|
|
XCTAssertTrue(expectedAlive.allSatisfy { !$0.isInvalidated })
|
|
}
|
|
|
|
func testCleanupMissingServers() throws {
|
|
let server3 = servers.addFake()
|
|
let api3 = FakeHomeAssistantAPI(server: server3)
|
|
Current.cachedApis[server3.identifier] = api3
|
|
|
|
let start1 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
let start3 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
|
|
manager.cleanup(definitions: [
|
|
.init(orphansOf: TestStoreModel1.self),
|
|
.init(orphansOf: TestStoreModel3.self),
|
|
]).cauterize()
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
realm.add(start3)
|
|
}
|
|
}
|
|
|
|
servers.remove(identifier: api2.server.identifier)
|
|
servers.notify()
|
|
|
|
try testQueue.sync {
|
|
let expected = Set(start1 + start3 + [start2[3]])
|
|
let present = Set<Object>(realm.objects(TestStoreModel1.self).map { $0 as Object })
|
|
.union(realm.objects(TestStoreModel3.self).map { $0 as Object })
|
|
XCTAssertEqual(present, expected)
|
|
|
|
XCTAssertEqual(
|
|
try XCTUnwrap(start2[3] as? TestStoreModel3).serverIdentifier,
|
|
api1.server.identifier.rawValue
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(Set(TestStoreModel1.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s2m1", "s2m2", "s2m3",
|
|
]))
|
|
XCTAssertEqual(Set(TestStoreModel3.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s2m5", "s2m6",
|
|
]))
|
|
}
|
|
|
|
func testFetchInvokesDefinition() {
|
|
let (fetchPromise1, fetchSeal1) = Promise<Void>.pending()
|
|
let (fetchPromise2, fetchSeal2) = Promise<Void>.pending()
|
|
|
|
var fetchApi1 = [HomeAssistantAPI]()
|
|
var fetchApi2 = [HomeAssistantAPI]()
|
|
|
|
let promise = manager.fetch(definitions: [
|
|
.init(update: { api, queue, manager -> Promise<Void> in
|
|
fetchApi1.append(api)
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
return fetchPromise1
|
|
}),
|
|
.init(update: { api, queue, manager -> Promise<Void> in
|
|
fetchApi2.append(api)
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
return fetchPromise2
|
|
}),
|
|
], apis: [api1, api2])
|
|
|
|
XCTAssertFalse(promise.isResolved)
|
|
fetchSeal1.fulfill(())
|
|
XCTAssertFalse(promise.isResolved)
|
|
fetchSeal2.fulfill(())
|
|
XCTAssertNoThrow(try hang(promise))
|
|
|
|
XCTAssertEqual(fetchApi1.map(\.server), [api1.server, api2.server])
|
|
XCTAssertEqual(fetchApi2.map(\.server), [api1.server, api2.server])
|
|
}
|
|
|
|
func testSubscribeSubscribes() {
|
|
let handlers1_1: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_1: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers1_2: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_2: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers1_3: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_3: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
|
|
var handlers1Iterator = handlers1_1.makeIterator()
|
|
var handlers2Iterator = handlers2_1.makeIterator()
|
|
|
|
var handlers1APIs = [(HAConnection, Server)]()
|
|
var handlers2APIs = [(HAConnection, Server)]()
|
|
|
|
let definitions: [ModelManager.SubscribeDefinition] = [
|
|
.init(subscribe: { connection, server, queue, manager -> [HACancellable] in
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
handlers1APIs.append((connection, server))
|
|
return [handlers1Iterator.next()!]
|
|
}),
|
|
.init(subscribe: { connection, server, queue, manager -> [HACancellable] in
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
handlers2APIs.append((connection, server))
|
|
return [handlers2Iterator.next()!]
|
|
}),
|
|
]
|
|
|
|
manager.subscribe(definitions: definitions, isAppInForeground: { true })
|
|
|
|
func verify(apis: [HomeAssistantAPI]) {
|
|
XCTAssertEqual(handlers1APIs.map(\.1), apis.map(\.server))
|
|
XCTAssertEqual(handlers2APIs.map(\.1), apis.map(\.server))
|
|
XCTAssertEqual(
|
|
handlers1APIs.map(\.0).map(ObjectIdentifier.init(_:)),
|
|
apis.compactMap(\.connection).map(ObjectIdentifier.init(_:))
|
|
)
|
|
}
|
|
|
|
verify(apis: [api1, api2])
|
|
|
|
XCTAssertTrue(handlers1_1.allSatisfy { !$0.wasCancelled })
|
|
XCTAssertTrue(handlers2_1.allSatisfy { !$0.wasCancelled })
|
|
|
|
handlers1Iterator = handlers1_2.makeIterator()
|
|
handlers2Iterator = handlers2_2.makeIterator()
|
|
|
|
handlers1APIs.removeAll()
|
|
handlers2APIs.removeAll()
|
|
|
|
manager.subscribe(definitions: definitions, isAppInForeground: { true })
|
|
|
|
XCTAssertTrue(handlers1_1.allSatisfy(\.wasCancelled))
|
|
XCTAssertTrue(handlers2_1.allSatisfy(\.wasCancelled))
|
|
|
|
XCTAssertTrue(handlers1_2.allSatisfy { !$0.wasCancelled })
|
|
XCTAssertTrue(handlers2_2.allSatisfy { !$0.wasCancelled })
|
|
|
|
verify(apis: [api1, api2])
|
|
|
|
servers.remove(identifier: api1.server.identifier)
|
|
let new = servers.addFake()
|
|
let newApi = FakeHomeAssistantAPI(server: new)
|
|
Current.cachedApis[new.identifier] = newApi
|
|
|
|
handlers1Iterator = handlers1_3.makeIterator()
|
|
handlers2Iterator = handlers2_3.makeIterator()
|
|
|
|
handlers1APIs.removeAll()
|
|
handlers2APIs.removeAll()
|
|
|
|
servers.notify()
|
|
|
|
XCTAssertTrue(handlers1_2.allSatisfy(\.wasCancelled))
|
|
XCTAssertTrue(handlers2_2.allSatisfy(\.wasCancelled))
|
|
|
|
verify(apis: [api2, newApi])
|
|
}
|
|
|
|
func testStoreWithoutModels() throws {
|
|
try testQueue.sync {
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: []))
|
|
XCTAssertTrue(realm.objects(TestStoreModel1.self).isEmpty)
|
|
}
|
|
}
|
|
|
|
func testStoreWithModelLackingPrimaryKey() throws {
|
|
func doStore() throws {
|
|
try testQueue.sync {
|
|
try hang(manager.store(type: TestStoreModel2.self, from: api1.server, sourceModels: []))
|
|
}
|
|
}
|
|
|
|
XCTAssertThrowsError(try doStore()) { error in
|
|
XCTAssertEqual(error as? ModelManager.StoreError, .missingPrimaryKey)
|
|
}
|
|
}
|
|
|
|
func testStoreWithoutExistingObjects() throws {
|
|
try testQueue.sync {
|
|
let sources1: [TestStoreSource1] = [
|
|
.init(id: "id1s1", value: "val1"),
|
|
.init(id: "id2s1", value: "val2"),
|
|
]
|
|
let sources2: [TestStoreSource1] = [
|
|
.init(id: "id1s2", value: "val1"),
|
|
.init(id: "id2s2", value: "val2"),
|
|
]
|
|
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: sources1))
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api2.server, sourceModels: sources2))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.identifier))
|
|
XCTAssertEqual(models.count, 4)
|
|
XCTAssertEqual(models[0].identifier, "s1/id1s1")
|
|
XCTAssertEqual(models[0].serverIdentifier, api1.server.identifier.rawValue)
|
|
XCTAssertEqual(models[0].value, "val1")
|
|
XCTAssertEqual(models[1].identifier, "s1/id2s1")
|
|
XCTAssertEqual(models[1].serverIdentifier, api1.server.identifier.rawValue)
|
|
XCTAssertEqual(models[1].value, "val2")
|
|
XCTAssertEqual(models[2].identifier, "s2/id1s2")
|
|
XCTAssertEqual(models[2].serverIdentifier, api2.server.identifier.rawValue)
|
|
XCTAssertEqual(models[2].value, "val1")
|
|
XCTAssertEqual(models[3].identifier, "s2/id2s2")
|
|
XCTAssertEqual(models[3].serverIdentifier, api2.server.identifier.rawValue)
|
|
XCTAssertEqual(models[3].value, "val2")
|
|
XCTAssertEqual(Set(TestStoreModel1.lastDidUpdates.flatMap { $0 }), Set(models))
|
|
}
|
|
}
|
|
|
|
func testStoreUpdatesAndDeletes() throws {
|
|
let start1 = [
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id1s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id2s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id3s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val3"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id4s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val4"
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id1s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id2s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id3s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val3"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id4s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val4"
|
|
},
|
|
]
|
|
|
|
let insertedSources1 = [
|
|
TestStoreSource1(id: "ins_id1s1", value: "ins_val1"),
|
|
TestStoreSource1(id: "ins_id2s1", value: "ins_val2"),
|
|
]
|
|
let insertedSources2 = [
|
|
TestStoreSource1(id: "ins_id1s2", value: "ins_val1"),
|
|
TestStoreSource1(id: "ins_id2s2", value: "ins_val2"),
|
|
]
|
|
|
|
let updatedSources1 = [
|
|
TestStoreSource1(id: "start_id1s1", value: "start_val1-2"),
|
|
TestStoreSource1(id: "start_id2s1", value: "start_val2-2"),
|
|
]
|
|
let updatedSources2 = [
|
|
TestStoreSource1(id: "start_id1s2", value: "start_val1-2"),
|
|
TestStoreSource1(id: "start_id2s2", value: "start_val2-2"),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
}
|
|
|
|
try hang(manager.store(
|
|
type: TestStoreModel1.self,
|
|
from: api1.server,
|
|
sourceModels: insertedSources1 + updatedSources1
|
|
))
|
|
try hang(manager.store(
|
|
type: TestStoreModel1.self,
|
|
from: api2.server,
|
|
sourceModels: insertedSources2 + updatedSources2
|
|
))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.value))
|
|
XCTAssertEqual(models.count, 8)
|
|
|
|
// inserted
|
|
XCTAssertEqual(models[0].identifier, "s1/ins_id1s1")
|
|
XCTAssertEqual(models[0].value, "ins_val1")
|
|
XCTAssertEqual(models[1].identifier, "s2/ins_id1s2")
|
|
XCTAssertEqual(models[1].value, "ins_val1")
|
|
XCTAssertEqual(models[2].identifier, "s1/ins_id2s1")
|
|
XCTAssertEqual(models[2].value, "ins_val2")
|
|
XCTAssertEqual(models[3].identifier, "s2/ins_id2s2")
|
|
XCTAssertEqual(models[3].value, "ins_val2")
|
|
|
|
// updated
|
|
XCTAssertEqual(models[4].identifier, "s1/start_id1s1")
|
|
XCTAssertEqual(models[4].value, "start_val1-2")
|
|
XCTAssertEqual(models[5].identifier, "s2/start_id1s2")
|
|
XCTAssertEqual(models[5].value, "start_val1-2")
|
|
XCTAssertEqual(models[6].identifier, "s1/start_id2s1")
|
|
XCTAssertEqual(models[6].value, "start_val2-2")
|
|
XCTAssertEqual(models[7].identifier, "s2/start_id2s2")
|
|
XCTAssertEqual(models[7].value, "start_val2-2")
|
|
|
|
// deleted
|
|
XCTAssertEqual(Set(TestStoreModel1.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s1/start_id3s1",
|
|
"s1/start_id4s1",
|
|
"s2/start_id3s2",
|
|
"s2/start_id4s2",
|
|
]))
|
|
}
|
|
}
|
|
|
|
func testIneligibleNotDeleted() throws {
|
|
let start1 = [
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id1s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 10 // eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id2s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 1 // not eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id3s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 100 // eligible, will be deleted
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id1s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 10 // eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id2s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 1 // not eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id3s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 100 // eligible, will be deleted
|
|
},
|
|
]
|
|
|
|
let insertedSources1 = [
|
|
TestStoreSource2(id: "ins_id1s1", value: 100),
|
|
]
|
|
let insertedSources2 = [
|
|
TestStoreSource2(id: "ins_id1s2", value: 100),
|
|
]
|
|
|
|
let updatedSources1 = [
|
|
TestStoreSource2(id: "start_id1s1", value: 4),
|
|
]
|
|
let updatedSources2 = [
|
|
TestStoreSource2(id: "start_id1s2", value: 4),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
}
|
|
|
|
try hang(manager.store(
|
|
type: TestStoreModel3.self,
|
|
from: api1.server,
|
|
sourceModels: insertedSources1 + updatedSources1
|
|
))
|
|
try hang(manager.store(
|
|
type: TestStoreModel3.self,
|
|
from: api2.server,
|
|
sourceModels: insertedSources2 + updatedSources2
|
|
))
|
|
let models = realm.objects(TestStoreModel3.self).sorted(byKeyPath: #keyPath(TestStoreModel3.value))
|
|
XCTAssertEqual(models.count, 6)
|
|
|
|
XCTAssertEqual(models[0].identifier, "s1/start_id2s1")
|
|
XCTAssertEqual(models[0].value, 1)
|
|
XCTAssertEqual(models[1].identifier, "s2/start_id2s2")
|
|
XCTAssertEqual(models[1].value, 1)
|
|
XCTAssertEqual(models[2].identifier, "s1/start_id1s1")
|
|
XCTAssertEqual(models[2].value, 4)
|
|
XCTAssertEqual(models[3].identifier, "s2/start_id1s2")
|
|
XCTAssertEqual(models[3].value, 4)
|
|
XCTAssertEqual(models[4].identifier, "s1/ins_id1s1")
|
|
XCTAssertEqual(models[4].value, 100)
|
|
XCTAssertEqual(models[5].identifier, "s2/ins_id1s2")
|
|
XCTAssertEqual(models[5].value, 100)
|
|
}
|
|
}
|
|
|
|
func testUpdateFalseSkipsNewCreation() throws {
|
|
try testQueue.sync {
|
|
let sources: [TestStoreSource1] = [
|
|
.init(id: "id1", value: "val1"),
|
|
.init(id: "id2", value: "val2"),
|
|
]
|
|
|
|
TestStoreModel1.updateFalseIds = ["id2"]
|
|
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: sources))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.identifier))
|
|
XCTAssertEqual(models.count, 1)
|
|
XCTAssertEqual(models[0].identifier, "s1/id1")
|
|
XCTAssertEqual(models[0].value, "val1")
|
|
XCTAssertEqual(Set(TestStoreModel1.lastDidUpdates.flatMap { $0 }), Set(models))
|
|
}
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel1: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel1.identifier)
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel2: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel2.identifier)
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel3: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
super.init()
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel3.identifier)
|
|
}
|
|
}
|
|
|
|
final class TestStoreModel1: Object, UpdatableModel {
|
|
static var updateFalseIds = [String]()
|
|
|
|
static var lastDidUpdates: [[TestStoreModel1]] = []
|
|
static var lastWillDeleteIds: [[String]] = []
|
|
static func didUpdate(objects: [TestStoreModel1], server: Server, realm: Realm) {
|
|
lastDidUpdates.append(objects)
|
|
}
|
|
|
|
static func willDelete(objects: [TestStoreModel1], server: Server?, realm: Realm) {
|
|
lastWillDeleteIds.append(objects.compactMap(\.identifier))
|
|
}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
@objc dynamic var value: String?
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestStoreModel1.identifier)
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel1.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource1,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
if self.realm == nil {
|
|
serverIdentifier = server.identifier.rawValue
|
|
} else {
|
|
XCTAssertEqual(serverIdentifier, server.identifier.rawValue)
|
|
}
|
|
value = object.value
|
|
|
|
if Self.updateFalseIds.contains(object.id) {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TestStoreSource1: UpdatableModelSource {
|
|
var primaryKey: String { id }
|
|
|
|
var id: String = UUID().uuidString
|
|
var value: String?
|
|
}
|
|
|
|
final class TestStoreModel2: Object, UpdatableModel {
|
|
static func didUpdate(objects: [TestStoreModel2], server: Server, realm: Realm) {}
|
|
|
|
static func willDelete(objects: [TestStoreModel2], server: Server?, realm: Realm) {}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
nil
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel2.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource1,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
XCTFail("not expected to be called in error scenario")
|
|
return false
|
|
}
|
|
}
|
|
|
|
struct TestStoreSource2: UpdatableModelSource {
|
|
var primaryKey: String { id }
|
|
|
|
var id: String = UUID().uuidString
|
|
var value: Int = 0
|
|
}
|
|
|
|
final class TestStoreModel3: Object, UpdatableModel {
|
|
static func didUpdate(objects: [TestStoreModel3], server: Server, realm: Realm) {}
|
|
|
|
static var lastWillDeleteIds: [[String]] = []
|
|
static func willDelete(objects: [TestStoreModel3], server: Server?, realm: Realm) {
|
|
lastWillDeleteIds.append(objects.compactMap(\.identifier))
|
|
}
|
|
|
|
static var updateEligiblePredicate: NSPredicate {
|
|
.init(format: "value > 5")
|
|
}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
@objc dynamic var value: Int = 0
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
"identifier"
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel3.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource2,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
if self.realm == nil {
|
|
serverIdentifier = server.identifier.rawValue
|
|
} else {
|
|
XCTAssertEqual(serverIdentifier, server.identifier.rawValue)
|
|
}
|
|
value = object.value
|
|
return true
|
|
}
|
|
}
|
|
|
|
private class FakeHomeAssistantAPI: HomeAssistantAPI {}
|