Base fixes and test harness

This commit is contained in:
alexandrev-tibco
2026-02-01 11:12:57 +01:00
parent f5b13ec924
commit e328767c4a
39 changed files with 3575 additions and 142 deletions
@@ -0,0 +1,172 @@
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()
}
}
}