diff --git a/Sources/CoreDataRepository/CoreDataRepository+Batch.swift b/Sources/CoreDataRepository/CoreDataRepository+Batch.swift index 79a7247..20c7aa9 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Batch.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Batch.swift @@ -22,11 +22,13 @@ extension CoreDataRepository { _ request: NSBatchInsertRequest, transactionAuthor: String? = nil ) async -> Result { - await context.performInScratchPad { scratchPad in - scratchPad.transactionAuthor = transactionAuthor + await context.performInScratchPad { [context] scratchPad in + context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchInsertResult else { + context.transactionAuthor = nil throw CoreDataRepositoryError.fetchedObjectFailedToCastToExpectedType } + context.transactionAuthor = nil return result } } @@ -127,11 +129,13 @@ extension CoreDataRepository { _ request: NSBatchUpdateRequest, transactionAuthor: String? = nil ) async -> Result { - await context.performInScratchPad { scratchPad in - scratchPad.transactionAuthor = transactionAuthor + await context.performInScratchPad { [context] scratchPad in + context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchUpdateResult else { + context.transactionAuthor = nil throw CoreDataRepositoryError.fetchedObjectFailedToCastToExpectedType } + context.transactionAuthor = nil return result } } @@ -193,11 +197,13 @@ extension CoreDataRepository { _ request: NSBatchDeleteRequest, transactionAuthor: String? = nil ) async -> Result { - await context.performInScratchPad { scratchPad in - scratchPad.transactionAuthor = transactionAuthor + await context.performInScratchPad { [context] scratchPad in + context.transactionAuthor = transactionAuthor guard let result = try scratchPad.execute(request) as? NSBatchDeleteResult else { + context.transactionAuthor = nil throw CoreDataRepositoryError.fetchedObjectFailedToCastToExpectedType } + context.transactionAuthor = nil return result } } diff --git a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift index dc227f7..78c95e8 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift @@ -27,12 +27,13 @@ extension CoreDataRepository { transactionAuthor: String? = nil ) async -> Result { 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 { + context.transactionAuthor = transactionAuthor try context.save() + context.transactionAuthor = nil } try scratchPad.obtainPermanentIDs(for: [object]) return object.asUnmanaged @@ -70,16 +71,19 @@ extension CoreDataRepository { public func update( _ url: URL, with item: Model, - transactionAuthor _: String? = nil + transactionAuthor: String? = nil ) async -> Result { await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in + scratchPad.transactionAuthor = transactionAuthor 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 { + context.transactionAuthor = transactionAuthor try context.save() + context.transactionAuthor = nil } return repoManaged.asUnmanaged } @@ -97,16 +101,19 @@ extension CoreDataRepository { /// public func delete( _ url: URL, - transactionAuthor _: String? = nil + transactionAuthor: String? = nil ) async -> Result { await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in + scratchPad.transactionAuthor = transactionAuthor 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 { + context.transactionAuthor = transactionAuthor try context.save() + context.transactionAuthor = nil } return () } diff --git a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift index 4c4f350..6159129 100644 --- a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift @@ -57,8 +57,12 @@ final class BatchRepositoryTests: CoreDataXCTestCase { XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.") } + let historyTimeStamp = Date() + let transactionAuthor: String = #function + let request = try NSBatchInsertRequest(entityName: XCTUnwrap(RepoMovie.entity().name), objects: movies) - let result: Result = try await repository().insert(request) + let result: Result = try await repository() + .insert(request, transactionAuthor: transactionAuthor) switch result { case .success: @@ -75,6 +79,8 @@ final class BatchRepositoryTests: CoreDataXCTestCase { "Inserted titles should match expectation" ) } + + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testInsertFailure() async throws { @@ -110,8 +116,12 @@ final class BatchRepositoryTests: CoreDataXCTestCase { XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.") } + let historyTimeStamp = Date() + let transactionAuthor: String = #function + let newMovies = try movies.map(mapDictToMovie(_:)) - let result: (success: [Movie], failed: [Movie]) = try await repository().create(newMovies) + let result: (success: [Movie], failed: [Movie]) = try await repository() + .create(newMovies, transactionAuthor: transactionAuthor) XCTAssertEqual(result.success.count, newMovies.count) XCTAssertEqual(result.failed.count, 0) @@ -128,6 +138,8 @@ final class BatchRepositoryTests: CoreDataXCTestCase { "Inserted titles should match expectation" ) } + + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testReadSuccess() async throws { @@ -167,7 +179,11 @@ final class BatchRepositoryTests: CoreDataXCTestCase { request.predicate = predicate request.propertiesToUpdate = ["title": "Updated!", "boxOffice": 1] - let _: Result = try await repository().update(request) + let historyTimeStamp = Date() + let transactionAuthor: String = #function + + let _: Result = try await repository() + .update(request, transactionAuthor: transactionAuthor) try await repositoryContext().perform { let data = try self.repositoryContext().fetch(fetchRequest) @@ -177,6 +193,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { "Updated titles should match request" ) } + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testAltUpdateSuccess() async throws { @@ -196,12 +213,18 @@ final class BatchRepositoryTests: CoreDataXCTestCase { let newTitles = ["ZA", "ZB", "ZC", "ZD", "ZE"] newTitles.enumerated().forEach { index, title in editedMovies[index].title = title } - let result: (success: [Movie], failed: [Movie]) = try await repository().update(editedMovies) + let historyTimeStamp = Date() + let transactionAuthor: String = #function + + let result: (success: [Movie], failed: [Movie]) = try await repository() + .update(editedMovies, transactionAuthor: transactionAuthor) XCTAssertEqual(result.success.count, movies.count) XCTAssertEqual(result.failed.count, 0) XCTAssertEqual(Set(editedMovies), Set(result.success)) + + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testDeleteSuccess() async throws { @@ -221,12 +244,17 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .entity().name ))) - let _: Result = try await repository().delete(request) + let historyTimeStamp = Date() + let transactionAuthor: String = #function + + let _: Result = try await repository() + .delete(request, transactionAuthor: transactionAuthor) try await repositoryContext().perform { let data = try self.repositoryContext().fetch(fetchRequest) XCTAssertEqual(data.map { $0.title ?? "" }.sorted(), [], "There should be no remaining values.") } + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testAltDeleteSuccess() async throws { @@ -242,7 +270,11 @@ final class BatchRepositoryTests: CoreDataXCTestCase { movies = repoMovies.map(\.asUnmanaged) } - let result: (success: [URL], failed: [URL]) = try await repository().delete(urls: movies.compactMap(\.url)) + let historyTimeStamp = Date() + let transactionAuthor: String = #function + + let result: (success: [URL], failed: [URL]) = try await repository() + .delete(urls: movies.compactMap(\.url), transactionAuthor: transactionAuthor) XCTAssertEqual(result.success.count, movies.count) XCTAssertEqual(result.failed.count, 0) @@ -251,5 +283,6 @@ final class BatchRepositoryTests: CoreDataXCTestCase { let data = try self.repositoryContext().fetch(fetchRequest) XCTAssertEqual(data.map { $0.title ?? "" }.sorted(), [], "There should be no remaining values.") } + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } } diff --git a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift index 40cdf74..5769ad7 100644 --- a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift @@ -14,8 +14,11 @@ import XCTest final class CRUDRepositoryTests: CoreDataXCTestCase { func testCreateSuccess() async throws { + let historyTimeStamp = Date() + let transactionAuthor: String = #function let movie = Movie(id: UUID(), title: "Create Success", releaseDate: Date(), boxOffice: 100) - let result: Result = try await repository().create(movie) + let result: Result = try await repository() + .create(movie, transactionAuthor: transactionAuthor) guard case let .success(resultMovie) = result else { XCTFail("Not expecting a failed result") return @@ -26,6 +29,7 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { XCTAssertNoDifference(tempResultMovie, movie) try await verify(resultMovie) + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testReadSuccess() async throws { @@ -92,8 +96,11 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { movie.title = "Update Success - Edited" + let historyTimeStamp = Date() + let transactionAuthor: String = #function + let result: Result = try await repository() - .update(XCTUnwrap(createdMovie.url), with: movie) + .update(XCTUnwrap(createdMovie.url), with: movie, transactionAuthor: transactionAuthor) guard case let .success(resultMovie) = result else { XCTFail("Not expecting a failed result") @@ -107,6 +114,7 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { XCTAssertNoDifference(tempResultMovie, movie) try await verify(resultMovie) + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testUpdateFailure() async throws { @@ -148,8 +156,11 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { return object.asUnmanaged } + let historyTimeStamp = Date() + let transactionAuthor: String = #function + let result: Result = try await repository() - .delete(XCTUnwrap(createdMovie.url)) + .delete(XCTUnwrap(createdMovie.url), transactionAuthor: transactionAuthor) switch result { case .success: @@ -157,6 +168,8 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { case .failure: XCTFail("Not expecting a failed result") } + + try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp) } func testDeleteFailure() async throws { diff --git a/Tests/CoreDataRepositoryTests/CoreDataStack.swift b/Tests/CoreDataRepositoryTests/CoreDataStack.swift index 6cca8fe..b8c228f 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataStack.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataStack.swift @@ -17,6 +17,7 @@ class CoreDataStack: NSObject { static var persistentContainer: NSPersistentContainer { let desc = NSPersistentStoreDescription() + desc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) desc.type = NSSQLiteStoreType // NSInMemoryStoreType desc.shouldAddStoreAsynchronously = false let model = Self.model diff --git a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift index 20b4637..3997703 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift @@ -86,4 +86,16 @@ class CoreDataXCTestCase: XCTestCase { XCTAssertNoDifference(item, managedItem.asUnmanaged) } } + + func verify(transactionAuthor: String?, timeStamp: Date) throws { + let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: timeStamp) + try repositoryContext().performAndWait { + let historyResult = try XCTUnwrap(repositoryContext().execute(historyRequest) as? NSPersistentHistoryResult) + let history = try XCTUnwrap(historyResult.result as? [NSPersistentHistoryTransaction]) + XCTAssertGreaterThan(history.count, 0) + history.forEach { historyTransaction in + XCTAssertEqual(historyTransaction.author, transactionAuthor) + } + } + } }