173 lines
5.5 KiB
Swift
173 lines
5.5 KiB
Swift
import CoreData
|
|
@testable import PortfolioJournal
|
|
|
|
/// Error types for TestCoreDataStack
|
|
enum TestCoreDataStackError: Error {
|
|
case modelNotFound
|
|
case storeLoadFailed(Error)
|
|
}
|
|
|
|
/// In-memory Core Data stack for testing purposes
|
|
/// This allows tests to run in isolation without affecting the actual database
|
|
final class TestCoreDataStack {
|
|
|
|
let persistentContainer: NSPersistentContainer
|
|
|
|
var viewContext: NSManagedObjectContext {
|
|
persistentContainer.viewContext
|
|
}
|
|
|
|
init() throws {
|
|
// Use the same approach as NSPersistentContainer(name:) which auto-finds the model
|
|
// When tests run with the app as TEST_HOST, Bundle.main is the app bundle
|
|
let modelName = "PortfolioJournal"
|
|
|
|
// Create the container - it will find the model in the main bundle
|
|
persistentContainer = NSPersistentContainer(name: modelName)
|
|
|
|
// Configure for in-memory storage
|
|
let description = NSPersistentStoreDescription()
|
|
description.type = NSInMemoryStoreType
|
|
description.shouldAddStoreAsynchronously = false
|
|
|
|
persistentContainer.persistentStoreDescriptions = [description]
|
|
|
|
var loadError: Error?
|
|
persistentContainer.loadPersistentStores { _, error in
|
|
loadError = error
|
|
}
|
|
|
|
if let error = loadError {
|
|
throw TestCoreDataStackError.storeLoadFailed(error)
|
|
}
|
|
|
|
viewContext.automaticallyMergesChangesFromParent = true
|
|
viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
}
|
|
|
|
/// Creates a fresh context for each test
|
|
func newBackgroundContext() -> NSManagedObjectContext {
|
|
let context = persistentContainer.newBackgroundContext()
|
|
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
return context
|
|
}
|
|
|
|
/// Resets the in-memory store (call in tearDown)
|
|
func reset() {
|
|
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Snapshot")
|
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
|
try? viewContext.execute(deleteRequest)
|
|
|
|
let sourceFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "InvestmentSource")
|
|
let sourceDelete = NSBatchDeleteRequest(fetchRequest: sourceFetch)
|
|
try? viewContext.execute(sourceDelete)
|
|
|
|
let categoryFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Category")
|
|
let categoryDelete = NSBatchDeleteRequest(fetchRequest: categoryFetch)
|
|
try? viewContext.execute(categoryDelete)
|
|
|
|
let accountFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Account")
|
|
let accountDelete = NSBatchDeleteRequest(fetchRequest: accountFetch)
|
|
try? viewContext.execute(accountDelete)
|
|
|
|
let goalFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Goal")
|
|
let goalDelete = NSBatchDeleteRequest(fetchRequest: goalFetch)
|
|
try? viewContext.execute(goalDelete)
|
|
|
|
viewContext.reset()
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Data Factory
|
|
|
|
extension TestCoreDataStack {
|
|
|
|
/// Creates a test account
|
|
@MainActor
|
|
func createTestAccount(
|
|
name: String = "Test Account",
|
|
currency: String = "USD"
|
|
) -> Account {
|
|
let account = Account(context: viewContext)
|
|
account.id = UUID()
|
|
account.name = name
|
|
account.currency = currency
|
|
account.createdAt = Date()
|
|
return account
|
|
}
|
|
|
|
/// Creates a test category
|
|
@MainActor
|
|
func createTestCategory(
|
|
name: String = "Test Category",
|
|
colorHex: String = "#3B82F6"
|
|
) -> PortfolioJournal.Category {
|
|
let category = PortfolioJournal.Category(context: viewContext)
|
|
category.id = UUID()
|
|
category.name = name
|
|
category.colorHex = colorHex
|
|
return category
|
|
}
|
|
|
|
/// Creates a test investment source
|
|
@MainActor
|
|
func createTestSource(
|
|
name: String = "Test Source",
|
|
account: Account? = nil,
|
|
category: PortfolioJournal.Category? = nil
|
|
) -> InvestmentSource {
|
|
let source = InvestmentSource(context: viewContext)
|
|
source.id = UUID()
|
|
source.name = name
|
|
source.createdAt = Date()
|
|
source.account = account
|
|
source.category = category
|
|
return source
|
|
}
|
|
|
|
/// Creates a test snapshot
|
|
@MainActor
|
|
func createTestSnapshot(
|
|
source: InvestmentSource,
|
|
value: Decimal,
|
|
date: Date = Date(),
|
|
contribution: Decimal? = nil
|
|
) -> Snapshot {
|
|
let snapshot = Snapshot(context: viewContext)
|
|
snapshot.id = UUID()
|
|
snapshot.value = NSDecimalNumber(decimal: value)
|
|
snapshot.date = date
|
|
snapshot.source = source
|
|
if let contribution = contribution {
|
|
snapshot.contribution = NSDecimalNumber(decimal: contribution)
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
/// Creates a test goal
|
|
@MainActor
|
|
func createTestGoal(
|
|
name: String = "Test Goal",
|
|
targetAmount: Decimal,
|
|
account: Account? = nil,
|
|
targetDate: Date? = nil
|
|
) -> Goal {
|
|
let goal = Goal(context: viewContext)
|
|
goal.id = UUID()
|
|
goal.name = name
|
|
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
|
|
goal.createdAt = Date()
|
|
goal.isActive = true
|
|
goal.account = account
|
|
goal.targetDate = targetDate
|
|
return goal
|
|
}
|
|
|
|
/// Saves the context
|
|
func save() throws {
|
|
if viewContext.hasChanges {
|
|
try viewContext.save()
|
|
}
|
|
}
|
|
}
|