From b6a4b9f8961baf14970030b4dde3aca3329a37b2 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 27 Oct 2022 22:00:27 -0500 Subject: [PATCH 1/3] Add save of repository's context after scratchpad save in mutating CRUD endpoints bugfix/create-returns-items-with-temporary-objectids --- .../CoreDataRepository+CRUD.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift index 3629cfa..95adcd6 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift @@ -26,11 +26,14 @@ extension CoreDataRepository { _ item: Model, transactionAuthor: String? = nil ) async -> Result { - await context.performInScratchPad(schedule: .enqueued) { scratchPad in + await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in scratchPad.transactionAuthor = transactionAuthor let object = Model.RepoManaged(context: scratchPad) object.create(from: item) try scratchPad.save() + try context.performAndWait { + try context.save() + } return object.asUnmanaged } } @@ -68,12 +71,15 @@ extension CoreDataRepository { with item: Model, transactionAuthor _: String? = nil ) async -> Result { - await context.performInScratchPad(schedule: .enqueued) { scratchPad in + await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in let id = try scratchPad.tryObjectId(from: url) let object = try scratchPad.notDeletedObject(for: id) let repoManaged: Model.RepoManaged = try object.asRepoManaged() repoManaged.update(from: item) try scratchPad.save() + try context.performAndWait { + try context.save() + } return repoManaged.asUnmanaged } } @@ -92,12 +98,15 @@ extension CoreDataRepository { _ url: URL, transactionAuthor _: String? = nil ) async -> Result { - await context.performInScratchPad(schedule: .enqueued) { scratchPad in + await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in let id = try scratchPad.tryObjectId(from: url) let object = try scratchPad.notDeletedObject(for: id) object.prepareForDeletion() scratchPad.delete(object) try scratchPad.save() + try context.performAndWait { + try context.save() + } return () } } From cd59a8e617072bfdae5857d9f560de588a6c8d73 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 27 Oct 2022 22:01:30 -0500 Subject: [PATCH 2/3] Add call to `scratchPad.obtainPermanentIDs` in CRUD create before mapping to unmanaged. bugfix/create-returns-items-with-temporary-objectids --- Sources/CoreDataRepository/CoreDataRepository+CRUD.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift index 95adcd6..10d9d36 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift @@ -34,6 +34,7 @@ extension CoreDataRepository { try context.performAndWait { try context.save() } + try scratchPad.obtainPermanentIDs(for: [object]) return object.asUnmanaged } } From 6dcf350c3b881c4686223e47f16f6727b152801b Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 27 Oct 2022 22:02:45 -0500 Subject: [PATCH 3/3] Add `CoreDataXCTestCase.verify` helper to more thoroughly check the store for an object after creation or mutation. bugfix/create-returns-items-with-temporary-objectids --- .../BatchRepositoryTests.swift | 4 ++ .../CRUDRepositoryTests.swift | 37 +++++++++++-------- .../CoreDataXCTestCase.swift | 37 +++++++++++++++++++ 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift index 8cd344d..062fa47 100644 --- a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift @@ -116,6 +116,10 @@ final class BatchRepositoryTests: CoreDataXCTestCase { XCTAssertEqual(result.success.count, newMovies.count) XCTAssertEqual(result.failed.count, 0) + for movie in result.success { + try await verify(movie) + } + try await repositoryContext().perform { let data = try self.repositoryContext().fetch(fetchRequest) XCTAssertEqual( diff --git a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift index eb0db38..1917141 100644 --- a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift @@ -16,15 +16,16 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testCreateSuccess() async throws { let movie = Movie(id: UUID(), title: "Create Success", releaseDate: Date(), boxOffice: 100) let result: Result = try await repository().create(movie) - guard case var .success(resultMovie) = result else { + guard case let .success(resultMovie) = result else { XCTFail("Not expecting a failed result") return } + var tempResultMovie = resultMovie + XCTAssertNotNil(tempResultMovie.url) + tempResultMovie.url = nil + XCTAssertNoDifference(tempResultMovie, movie) - XCTAssertNotNil(resultMovie.url) - resultMovie.url = nil - let diff = CustomDump.diff(resultMovie, movie) - XCTAssertNil(diff) + try await verify(resultMovie) } func testReadSuccess() async throws { @@ -39,15 +40,18 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { let result: Result = try await repository() .read(try XCTUnwrap(createdMovie.url)) - guard case var .success(resultMovie) = result else { + guard case let .success(resultMovie) = result else { XCTFail("Not expecting a failed result") return } - XCTAssertNotNil(resultMovie.url) - resultMovie.url = nil - let diff = CustomDump.diff(resultMovie, movie) - XCTAssertNil(diff) + var tempResultMovie = resultMovie + + XCTAssertNotNil(tempResultMovie.url) + tempResultMovie.url = nil + XCTAssertNoDifference(tempResultMovie, movie) + + try await verify(resultMovie) } func testReadFailure() async throws { @@ -91,15 +95,18 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { let result: Result = try await repository() .update(try XCTUnwrap(createdMovie.url), with: movie) - guard case var .success(resultMovie) = result else { + guard case let .success(resultMovie) = result else { XCTFail("Not expecting a failed result") return } - XCTAssertNotNil(resultMovie.url) - resultMovie.url = nil - let diff = CustomDump.diff(resultMovie, movie) - XCTAssertNil(diff) + var tempResultMovie = resultMovie + + XCTAssertNotNil(tempResultMovie.url) + tempResultMovie.url = nil + XCTAssertNoDifference(tempResultMovie, movie) + + try await verify(resultMovie) } func testUpdateFailure() async throws { diff --git a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift index 6320d06..103fcf7 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift @@ -9,6 +9,7 @@ import Combine import CoreData import CoreDataRepository +import CustomDump import XCTest class CoreDataXCTestCase: XCTestCase { @@ -49,4 +50,40 @@ class CoreDataXCTestCase: XCTestCase { _repository = nil cancellables.forEach { $0.cancel() } } + + func verify(_ item: T) async throws where T: UnmanagedModel { + guard let url = item.managedRepoUrl else { + XCTFail("Failed to verify item in store because it has no URL") + return + } + + let context = try repositoryContext() + let coordinator = try container().persistentStoreCoordinator + context.performAndWait { + guard let objectID = coordinator.managedObjectID(forURIRepresentation: url) else { + XCTFail("Failed to verify item in store because no NSManagedObjectID found in viewContext from URL.") + return + } + var _object: NSManagedObject? + do { + _object = try context.existingObject(with: objectID) + } catch { + XCTFail( + "Failed to verify item in store because it was not found by its NSManagedObjectID. Error: \(error.localizedDescription)" + ) + return + } + + guard let object = _object else { + XCTFail("Failed to verify item in store because it was not found by its NSManagedObjectID") + return + } + + guard let managedItem = object as? T.RepoManaged else { + XCTFail("Failed to verify item in store because it failed to cast to RepoManaged type.") + return + } + XCTAssertNoDifference(item, managedItem.asUnmanaged) + } + } }