// // NotificationDeliveryDAOTests.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 NotificationDeliveryDAO * * Tests CRUD operations, query helpers, relationships, and cascade delete */ class NotificationDeliveryDAOTests: 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 deliveryTimestamp = Date() let id = UUID().uuidString let notificationId = UUID().uuidString // When: Create entity let entity = NotificationDelivery.create( in: context, id: id, notificationId: notificationId, timesafariDid: "test-did", deliveryTimestamp: deliveryTimestamp, deliveryStatus: "delivered", deliveryMethod: "local", deliveryAttemptNumber: 1, deliveryDurationMs: 100, userInteractionType: "tap", userInteractionTimestamp: deliveryTimestamp, userInteractionDurationMs: 50, errorCode: nil, errorMessage: nil, deviceInfo: "{\"model\":\"iPhone\"}", networkInfo: "{\"type\":\"wifi\"}", batteryLevel: 80, dozeModeActive: false, exactAlarmPermission: true, notificationPermission: true, metadata: "{\"key\":\"value\"}" ) // Then: Entity should be created with correct values XCTAssertNotNil(entity, "Entity should be created") XCTAssertEqual(entity.id, id) XCTAssertEqual(entity.notificationId, notificationId) XCTAssertEqual(entity.timesafariDid, "test-did") XCTAssertEqual(entity.deliveryTimestamp, deliveryTimestamp) XCTAssertEqual(entity.deliveryStatus, "delivered") XCTAssertEqual(entity.deliveryMethod, "local") XCTAssertEqual(entity.deliveryAttemptNumber, 1) XCTAssertEqual(entity.deliveryDurationMs, 100) XCTAssertEqual(entity.userInteractionType, "tap") XCTAssertEqual(entity.userInteractionTimestamp, deliveryTimestamp) XCTAssertEqual(entity.userInteractionDurationMs, 50) XCTAssertEqual(entity.batteryLevel, 80) XCTAssertEqual(entity.dozeModeActive, false) XCTAssertEqual(entity.exactAlarmPermission, true) XCTAssertEqual(entity.notificationPermission, true) } func testCreate_WithRelationship() { // Given: NotificationContent entity let notificationId = UUID().uuidString let notification = NotificationContent.create( in: context, id: notificationId, scheduledTime: Date() ) try! context.save() // When: Create delivery with relationship let delivery = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, notificationContent: notification, deliveryTimestamp: Date() ) try! context.save() // Then: Relationship should be set XCTAssertNotNil(delivery.notificationContent, "Relationship should be set") XCTAssertEqual(delivery.notificationContent?.id, notificationId) // Verify inverse relationship XCTAssertTrue(notification.deliveries?.contains(delivery) ?? false, "Inverse relationship should be set") } func testCreate_FromDictionary_WithEpochMillis() { // Given: Dictionary with epoch milliseconds let deliveryTimestampMillis: Int64 = 1609459200000 let dict: [String: Any] = [ "id": "test-id", "notificationId": "notif-id", "deliveryTimestamp": deliveryTimestampMillis, "deliveryStatus": "delivered", "deliveryAttemptNumber": 1, "batteryLevel": 80 ] // When: Create from dictionary let entity = NotificationDelivery.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.notificationId, "notif-id") XCTAssertEqual(entity.deliveryStatus, "delivered") XCTAssertEqual(entity.deliveryAttemptNumber, 1) XCTAssertEqual(entity.batteryLevel, 80) // Verify date conversion let expectedDate = DailyNotificationDataConversions.dateFromEpochMillis(deliveryTimestampMillis) XCTAssertEqual(entity.deliveryTimestamp, expectedDate) } // MARK: - Read/Query Tests func testFetch_ById_Found() { // Given: Entity in database let id = UUID().uuidString let entity = NotificationDelivery.create( in: context, id: id, notificationId: UUID().uuidString, deliveryTimestamp: Date() ) try! context.save() // When: Fetch by id let fetched = NotificationDelivery.fetch(by: id, in: context) // Then: Should find entity XCTAssertNotNil(fetched, "Should find entity") XCTAssertEqual(fetched?.id, id) } func testQuery_ByNotificationId() { // Given: Deliveries for different notifications let notificationId1 = UUID().uuidString let notificationId2 = UUID().uuidString NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId1, deliveryTimestamp: Date() ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId1, deliveryTimestamp: Date() ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId2, deliveryTimestamp: Date() ) try! context.save() // When: Query by notificationId let results = NotificationDelivery.query(by: notificationId1, in: context) // Then: Should find only matching deliveries XCTAssertEqual(results.count, 2, "Should find 2 deliveries") XCTAssertTrue(results.allSatisfy { $0.notificationId == notificationId1 }) } func testQuery_DeliveryTimestampBetween() { // Given: Deliveries with different timestamps let startDate = Date() let midDate = startDate.addingTimeInterval(3600) // 1 hour later let endDate = startDate.addingTimeInterval(7200) // 2 hours later let outsideDate = startDate.addingTimeInterval(10800) // 3 hours later NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: startDate ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: midDate ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: endDate ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: outsideDate ) try! context.save() // When: Query by deliveryTimestamp range let results = NotificationDelivery.query( deliveryTimestampBetween: startDate, and: endDate, in: context ) // Then: Should find deliveries in range XCTAssertEqual(results.count, 3, "Should find 3 deliveries in range") XCTAssertTrue(results.allSatisfy { $0.deliveryTimestamp! >= startDate && $0.deliveryTimestamp! <= endDate }) } func testQuery_ByDeliveryStatus() { // Given: Deliveries with different statuses NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryStatus: "delivered", deliveryTimestamp: Date() ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryStatus: "delivered", deliveryTimestamp: Date() ) NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryStatus: "failed", deliveryTimestamp: Date() ) try! context.save() // When: Query by deliveryStatus let results = NotificationDelivery.query(by: "delivered", in: context) // Then: Should find only matching deliveries XCTAssertEqual(results.count, 2, "Should find 2 deliveries") XCTAssertTrue(results.allSatisfy { $0.deliveryStatus == "delivered" }) } // MARK: - Relationship Tests func testRelationship_OneToMany() { // Given: NotificationContent with multiple deliveries let notificationId = UUID().uuidString let notification = NotificationContent.create( in: context, id: notificationId, scheduledTime: Date() ) let delivery1 = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, notificationContent: notification, deliveryTimestamp: Date() ) let delivery2 = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, notificationContent: notification, deliveryTimestamp: Date() ) try! context.save() // Then: Notification should have multiple deliveries let deliveries = notification.deliveries as? Set XCTAssertNotNil(deliveries, "Deliveries should be available") XCTAssertEqual(deliveries?.count, 2, "Should have 2 deliveries") XCTAssertTrue(deliveries?.contains(delivery1) ?? false) XCTAssertTrue(deliveries?.contains(delivery2) ?? false) } // MARK: - Cascade Delete Tests func testCascadeDelete_WhenNotificationContentDeleted() { // Given: NotificationContent with deliveries let notificationId = UUID().uuidString let notification = NotificationContent.create( in: context, id: notificationId, scheduledTime: Date() ) let delivery1 = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, notificationContent: notification, deliveryTimestamp: Date() ) let delivery2 = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, notificationContent: notification, deliveryTimestamp: Date() ) try! context.save() // Verify deliveries exist let deliveriesBefore = NotificationDelivery.query(by: notificationId, in: context) XCTAssertEqual(deliveriesBefore.count, 2, "Should have 2 deliveries") // When: Delete NotificationContent NotificationContent.delete(by: notificationId, in: context) // Then: Deliveries should be cascade deleted let deliveriesAfter = NotificationDelivery.query(by: notificationId, in: context) XCTAssertEqual(deliveriesAfter.count, 0, "Deliveries should be cascade deleted") // Verify deliveries are actually deleted XCTAssertNil(NotificationDelivery.fetch(by: delivery1.id!, in: context)) XCTAssertNil(NotificationDelivery.fetch(by: delivery2.id!, in: context)) } // MARK: - Update Tests func testUpdateDeliveryStatus() { // Given: Entity with initial status let entity = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryStatus: "pending", deliveryTimestamp: Date() ) try! context.save() // When: Update delivery status entity.updateDeliveryStatus("delivered") try! context.save() // Then: Status should be updated XCTAssertEqual(entity.deliveryStatus, "delivered") } func testRecordUserInteraction() { // Given: Entity without interaction let entity = NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: Date() ) try! context.save() // When: Record user interaction let interactionTime = Date() entity.recordUserInteraction( type: "tap", timestamp: interactionTime, durationMs: 100 ) try! context.save() // Then: Interaction should be recorded XCTAssertEqual(entity.userInteractionType, "tap") XCTAssertEqual(entity.userInteractionTimestamp, interactionTime) XCTAssertEqual(entity.userInteractionDurationMs, 100) } // MARK: - Delete Tests func testDelete_ById_Found() { // Given: Entity in database let id = UUID().uuidString NotificationDelivery.create( in: context, id: id, notificationId: UUID().uuidString, deliveryTimestamp: Date() ) try! context.save() // When: Delete by id let deleted = NotificationDelivery.delete(by: id, in: context) // Then: Should be deleted XCTAssertTrue(deleted, "Should delete entity") // Verify deleted let fetched = NotificationDelivery.fetch(by: id, in: context) XCTAssertNil(fetched, "Entity should be deleted") } func testDeleteAll_ForNotificationId() { // Given: Multiple deliveries for a notification let notificationId = UUID().uuidString for i in 1...3 { NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: notificationId, deliveryTimestamp: Date() ) } try! context.save() // When: Delete all for notification let count = NotificationDelivery.deleteAll(for: notificationId, in: context) // Then: Should delete all XCTAssertEqual(count, 3, "Should delete 3 deliveries") // Verify all deleted let remaining = NotificationDelivery.query(by: notificationId, in: context) XCTAssertEqual(remaining.count, 0, "Should be empty") } func testDeleteAll() { // Given: Multiple deliveries for i in 1...5 { NotificationDelivery.create( in: context, id: UUID().uuidString, notificationId: UUID().uuidString, deliveryTimestamp: Date() ) } try! context.save() // When: Delete all let count = NotificationDelivery.deleteAll(in: context) // Then: Should delete all XCTAssertEqual(count, 5, "Should delete 5 deliveries") // Verify all deleted let all = NotificationDelivery.fetchAll(in: context) XCTAssertEqual(all.count, 0, "Should be empty") } }