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 = NSFetchRequest(entityName: "Snapshot") let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) try? viewContext.execute(deleteRequest) let sourceFetch: NSFetchRequest = NSFetchRequest(entityName: "InvestmentSource") let sourceDelete = NSBatchDeleteRequest(fetchRequest: sourceFetch) try? viewContext.execute(sourceDelete) let categoryFetch: NSFetchRequest = NSFetchRequest(entityName: "Category") let categoryDelete = NSBatchDeleteRequest(fetchRequest: categoryFetch) try? viewContext.execute(categoryDelete) let accountFetch: NSFetchRequest = NSFetchRequest(entityName: "Account") let accountDelete = NSBatchDeleteRequest(fetchRequest: accountFetch) try? viewContext.execute(accountDelete) let goalFetch: NSFetchRequest = 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() } } }