// // NotificationConfigDAOTests.swift // DailyNotificationPluginTests // // Created by Matthew Raymer on 2025-12-08 // Copyright © 2025 TimeSafari. All rights reserved. // import XCTest import CoreData @testable import DailyNotificationPlugin /** * Unit tests for NotificationConfigDAO * * Tests CRUD operations and query helpers for configuration management */ class NotificationConfigDAOTests: XCTestCase { var persistenceController: PersistenceController! var context: NSManagedObjectContext! override func setUp() { super.setUp() // Create in-memory Core Data stack persistenceController = PersistenceController(inMemory: true) context = persistenceController.viewContext XCTAssertNotNil(context, "Context should be available") } override func tearDown() { context = nil persistenceController = nil super.tearDown() } // MARK: - Create/Insert Tests func testCreate_WithAllParameters() { // Given: All parameters let id = UUID().uuidString // When: Create entity let entity = NotificationConfig.create( in: context, id: id, timesafariDid: "test-did", configType: "notification", configKey: "sound_enabled", configValue: "true", configDataType: "bool", isEncrypted: false, encryptionKeyId: nil, ttlSeconds: 86400, isActive: true, metadata: "{\"key\":\"value\"}" ) // Then: Entity should be created with correct values XCTAssertNotNil(entity, "Entity should be created") XCTAssertEqual(entity.id, id) XCTAssertEqual(entity.timesafariDid, "test-did") XCTAssertEqual(entity.configType, "notification") XCTAssertEqual(entity.configKey, "sound_enabled") XCTAssertEqual(entity.configValue, "true") XCTAssertEqual(entity.configDataType, "bool") XCTAssertEqual(entity.isEncrypted, false) XCTAssertEqual(entity.ttlSeconds, 86400) XCTAssertEqual(entity.isActive, true) XCTAssertNotNil(entity.createdAt) XCTAssertNotNil(entity.updatedAt) } func testCreate_WithMinimalParameters() { // Given: Minimal parameters (only required id) let id = UUID().uuidString // When: Create entity let entity = NotificationConfig.create( in: context, id: id ) // Then: Entity should be created with defaults XCTAssertNotNil(entity, "Entity should be created") XCTAssertEqual(entity.id, id) XCTAssertEqual(entity.isEncrypted, false) // Default XCTAssertEqual(entity.ttlSeconds, 604800) // Default (7 days) XCTAssertEqual(entity.isActive, true) // Default XCTAssertNotNil(entity.createdAt) XCTAssertNotNil(entity.updatedAt) } func testCreate_FromDictionary_WithEpochMillis() { // Given: Dictionary with epoch milliseconds let createdAtMillis: Int64 = 1609459200000 let dict: [String: Any] = [ "id": "test-id", "configKey": "test_key", "configValue": "test_value", "createdAt": createdAtMillis, "isActive": true ] // When: Create from dictionary let entity = NotificationConfig.create(in: context, from: dict) // Then: Entity should be created with converted dates XCTAssertNotNil(entity, "Entity should be created") XCTAssertEqual(entity.id, "test-id") XCTAssertEqual(entity.configKey, "test_key") XCTAssertEqual(entity.configValue, "test_value") XCTAssertEqual(entity.isActive, true) // Verify date conversion let expectedDate = DailyNotificationDataConversions.dateFromEpochMillis(createdAtMillis) XCTAssertEqual(entity.createdAt, expectedDate) } func testCreate_FromDictionary_MissingRequiredId() { // Given: Dictionary without required id let dict: [String: Any] = [ "configKey": "test_key" ] // When: Create from dictionary let entity = NotificationConfig.create(in: context, from: dict) // Then: Should be nil XCTAssertNil(entity, "Missing id should produce nil") } // MARK: - Read/Query Tests func testFetch_ById_Found() { // Given: Entity in database let id = UUID().uuidString let entity = NotificationConfig.create( in: context, id: id ) try! context.save() // When: Fetch by id let fetched = NotificationConfig.fetch(by: id, in: context) // Then: Should find entity XCTAssertNotNil(fetched, "Should find entity") XCTAssertEqual(fetched?.id, id) } func testFetch_ById_NotFound() { // Given: No entity in database // When: Fetch by non-existent id let fetched = NotificationConfig.fetch(by: "non-existent", in: context) // Then: Should be nil XCTAssertNil(fetched, "Should not find entity") } func testFetch_ByConfigKey_Found() { // Given: Entity with configKey let configKey = "sound_enabled" let entity = NotificationConfig.create( in: context, id: UUID().uuidString, configKey: configKey ) try! context.save() // When: Fetch by configKey let fetched = NotificationConfig.fetch(by: configKey, in: context) // Then: Should find entity XCTAssertNotNil(fetched, "Should find entity") XCTAssertEqual(fetched?.configKey, configKey) } func testFetch_ByConfigKey_NotFound() { // Given: No entity in database // When: Fetch by non-existent configKey let fetched = NotificationConfig.fetch(by: "non-existent", in: context) // Then: Should be nil XCTAssertNil(fetched, "Should not find entity") } func testFetchAll_Empty() { // Given: Empty database // When: Fetch all let all = NotificationConfig.fetchAll(in: context) // Then: Should be empty XCTAssertEqual(all.count, 0, "Should be empty") } func testFetchAll_WithEntities() { // Given: Multiple entities for i in 1...5 { NotificationConfig.create( in: context, id: "id-\(i)" ) } try! context.save() // When: Fetch all let all = NotificationConfig.fetchAll(in: context) // Then: Should find all XCTAssertEqual(all.count, 5, "Should find all entities") } func testQuery_ByTimesafariDid() { // Given: Entities with different timesafariDid NotificationConfig.create( in: context, id: "id-1", timesafariDid: "did-1" ) NotificationConfig.create( in: context, id: "id-2", timesafariDid: "did-1" ) NotificationConfig.create( in: context, id: "id-3", timesafariDid: "did-2" ) try! context.save() // When: Query by timesafariDid let results = NotificationConfig.query(by: "did-1", in: context) // Then: Should find only matching entities XCTAssertEqual(results.count, 2, "Should find 2 entities") XCTAssertTrue(results.allSatisfy { $0.timesafariDid == "did-1" }) } func testQuery_ByConfigType() { // Given: Entities with different config types NotificationConfig.create( in: context, id: "id-1", configType: "notification" ) NotificationConfig.create( in: context, id: "id-2", configType: "notification" ) NotificationConfig.create( in: context, id: "id-3", configType: "scheduling" ) try! context.save() // When: Query by configType let results = NotificationConfig.query(by: "notification", in: context) // Then: Should find only matching entities XCTAssertEqual(results.count, 2, "Should find 2 entities") XCTAssertTrue(results.allSatisfy { $0.configType == "notification" }) } func testQueryActive() { // Given: Entities with different active states NotificationConfig.create( in: context, id: "id-1", isActive: true ) NotificationConfig.create( in: context, id: "id-2", isActive: true ) NotificationConfig.create( in: context, id: "id-3", isActive: false ) try! context.save() // When: Query active let results = NotificationConfig.queryActive(in: context) // Then: Should find only active entities XCTAssertEqual(results.count, 2, "Should find 2 active entities") XCTAssertTrue(results.allSatisfy { $0.isActive == true }) } func testQuery_ByConfigTypeAndIsActive() { // Given: Entities with different types and active states NotificationConfig.create( in: context, id: "id-1", configType: "notification", isActive: true ) NotificationConfig.create( in: context, id: "id-2", configType: "notification", isActive: true ) NotificationConfig.create( in: context, id: "id-3", configType: "notification", isActive: false ) NotificationConfig.create( in: context, id: "id-4", configType: "scheduling", isActive: true ) try! context.save() // When: Query by configType and isActive let results = NotificationConfig.query( by: "notification", isActive: true, in: context ) // Then: Should find only matching entities XCTAssertEqual(results.count, 2, "Should find 2 entities") XCTAssertTrue(results.allSatisfy { $0.configType == "notification" && $0.isActive == true }) } // MARK: - Update Tests func testUpdateValue() { // Given: Entity with initial value let entity = NotificationConfig.create( in: context, id: UUID().uuidString, configValue: "old_value" ) let originalUpdatedAt = entity.updatedAt try! context.save() // Wait a bit to ensure time difference Thread.sleep(forTimeInterval: 0.1) // When: Update value entity.updateValue("new_value") try! context.save() // Then: Value and updatedAt should be updated XCTAssertEqual(entity.configValue, "new_value") XCTAssertNotNil(entity.updatedAt) XCTAssertGreaterThan(entity.updatedAt!, originalUpdatedAt!) } func testSetActive() { // Given: Entity with initial active state let entity = NotificationConfig.create( in: context, id: UUID().uuidString, isActive: true ) try! context.save() // When: Set inactive entity.setActive(false) try! context.save() // Then: Active state should be updated XCTAssertEqual(entity.isActive, false) } func testTouch_UpdatesUpdatedAt() { // Given: Entity with original updatedAt let entity = NotificationConfig.create( in: context, id: UUID().uuidString ) let originalUpdatedAt = entity.updatedAt try! context.save() // Wait a bit to ensure time difference Thread.sleep(forTimeInterval: 0.1) // When: Touch entity entity.touch() try! context.save() // Then: updatedAt should be newer XCTAssertNotNil(entity.updatedAt) XCTAssertGreaterThan(entity.updatedAt!, originalUpdatedAt!) } // MARK: - Delete Tests func testDelete_ById_Found() { // Given: Entity in database let id = UUID().uuidString NotificationConfig.create( in: context, id: id ) try! context.save() // When: Delete by id let deleted = NotificationConfig.delete(by: id, in: context) // Then: Should be deleted XCTAssertTrue(deleted, "Should delete entity") // Verify deleted let fetched = NotificationConfig.fetch(by: id, in: context) XCTAssertNil(fetched, "Entity should be deleted") } func testDelete_ByConfigKey_Found() { // Given: Entity with configKey let configKey = "sound_enabled" NotificationConfig.create( in: context, id: UUID().uuidString, configKey: configKey ) try! context.save() // When: Delete by configKey let deleted = NotificationConfig.delete(by: configKey, in: context) // Then: Should be deleted XCTAssertTrue(deleted, "Should delete entity") // Verify deleted let fetched = NotificationConfig.fetch(by: configKey, in: context) XCTAssertNil(fetched, "Entity should be deleted") } func testDeleteAll() { // Given: Multiple entities for i in 1...5 { NotificationConfig.create( in: context, id: "id-\(i)" ) } try! context.save() // When: Delete all let count = NotificationConfig.deleteAll(in: context) // Then: Should delete all XCTAssertEqual(count, 5, "Should delete 5 entities") // Verify all deleted let all = NotificationConfig.fetchAll(in: context) XCTAssertEqual(all.count, 0, "Should be empty") } }