Implement comprehensive data access layer for Core Data entities: - Add NotificationContentDAO, NotificationDeliveryDAO, and NotificationConfigDAO with full CRUD operations and query helpers - Add DailyNotificationDataConversions utility for type conversions (Date ↔ Int64, Int ↔ Int32, JSON, optional strings) - Update PersistenceController with entity verification and migration policies - Add comprehensive unit tests for all DAO classes and data conversions - Update Core Data model with NotificationContent, NotificationDelivery, and NotificationConfig entities (relationships and indexes) - Integrate ReactivationManager into DailyNotificationPlugin.load() DAO Features: - Create/Insert methods with dictionary support - Read/Query methods with predicates (by timesafariDid, notificationType, scheduledTime range, deliveryStatus, etc.) - Update methods (touch, updateDeliveryStatus, recordUserInteraction) - Delete methods (by ID, by key, delete all) - Relationship management (NotificationContent ↔ NotificationDelivery) - Cascade delete support Test Coverage: - 328 lines: DailyNotificationDataConversionsTests (time, numeric, string, JSON) - 490 lines: NotificationContentDAOTests (CRUD, queries, updates) - 415 lines: NotificationDeliveryDAOTests (CRUD, relationships, cascade delete) - 412 lines: NotificationConfigDAOTests (CRUD, queries, active filtering) All tests use in-memory Core Data stack for isolation and speed. Completes sections 4.4, 4.5, and 6.0 of iOS implementation checklist.
328 lines
12 KiB
Swift
328 lines
12 KiB
Swift
//
|
|
// DailyNotificationDataConversionsTests.swift
|
|
// DailyNotificationPluginTests
|
|
//
|
|
// Created by Matthew Raymer on 2025-12-08
|
|
// Copyright © 2025 TimeSafari. All rights reserved.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import DailyNotificationPlugin
|
|
|
|
/**
|
|
* Unit tests for DailyNotificationDataConversions
|
|
*
|
|
* Tests all data type conversion helpers:
|
|
* - Time conversions (Date ↔ Int64)
|
|
* - Numeric conversions (Int ↔ Int32, Int64 ↔ Int32)
|
|
* - String conversions (optional handling, JSON)
|
|
*/
|
|
class DailyNotificationDataConversionsTests: XCTestCase {
|
|
|
|
// MARK: - Time Conversion Tests
|
|
|
|
func testDateFromEpochMillis() {
|
|
// Given: Epoch milliseconds
|
|
let epochMillis: Int64 = 1609459200000 // 2021-01-01 00:00:00 UTC
|
|
|
|
// When: Convert to Date
|
|
let date = DailyNotificationDataConversions.dateFromEpochMillis(epochMillis)
|
|
|
|
// Then: Should match expected date
|
|
let expectedDate = Date(timeIntervalSince1970: 1609459200.0)
|
|
XCTAssertEqual(date.timeIntervalSince1970, expectedDate.timeIntervalSince1970,
|
|
accuracy: 0.001, "Date conversion should be accurate")
|
|
}
|
|
|
|
func testEpochMillisFromDate() {
|
|
// Given: Date
|
|
let date = Date(timeIntervalSince1970: 1609459200.0) // 2021-01-01 00:00:00 UTC
|
|
|
|
// When: Convert to epoch milliseconds
|
|
let epochMillis = DailyNotificationDataConversions.epochMillisFromDate(date)
|
|
|
|
// Then: Should match expected milliseconds
|
|
XCTAssertEqual(epochMillis, 1609459200000, "Epoch milliseconds should match")
|
|
}
|
|
|
|
func testDateFromEpochMillis_RoundTrip() {
|
|
// Given: Original epoch milliseconds
|
|
let originalMillis: Int64 = 1609459200000
|
|
|
|
// When: Convert to Date and back
|
|
let date = DailyNotificationDataConversions.dateFromEpochMillis(originalMillis)
|
|
let convertedMillis = DailyNotificationDataConversions.epochMillisFromDate(date)
|
|
|
|
// Then: Should match original
|
|
XCTAssertEqual(convertedMillis, originalMillis, "Round trip conversion should preserve value")
|
|
}
|
|
|
|
func testDateFromEpochMillis_Optional_Nil() {
|
|
// Given: Nil optional
|
|
let optionalMillis: Int64? = nil
|
|
|
|
// When: Convert to optional Date
|
|
let date = DailyNotificationDataConversions.dateFromEpochMillis(optionalMillis)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(date, "Nil input should produce nil output")
|
|
}
|
|
|
|
func testDateFromEpochMillis_Optional_Value() {
|
|
// Given: Optional with value
|
|
let optionalMillis: Int64? = 1609459200000
|
|
|
|
// When: Convert to optional Date
|
|
let date = DailyNotificationDataConversions.dateFromEpochMillis(optionalMillis)
|
|
|
|
// Then: Should have value
|
|
XCTAssertNotNil(date, "Non-nil input should produce non-nil output")
|
|
XCTAssertEqual(date!.timeIntervalSince1970, 1609459200.0, accuracy: 0.001)
|
|
}
|
|
|
|
func testEpochMillisFromDate_Optional_Nil() {
|
|
// Given: Nil optional Date
|
|
let optionalDate: Date? = nil
|
|
|
|
// When: Convert to optional milliseconds
|
|
let millis = DailyNotificationDataConversions.epochMillisFromDate(optionalDate)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(millis, "Nil input should produce nil output")
|
|
}
|
|
|
|
func testEpochMillisFromDate_Optional_Value() {
|
|
// Given: Optional Date with value
|
|
let optionalDate: Date? = Date(timeIntervalSince1970: 1609459200.0)
|
|
|
|
// When: Convert to optional milliseconds
|
|
let millis = DailyNotificationDataConversions.epochMillisFromDate(optionalDate)
|
|
|
|
// Then: Should have value
|
|
XCTAssertNotNil(millis, "Non-nil input should produce non-nil output")
|
|
XCTAssertEqual(millis, 1609459200000)
|
|
}
|
|
|
|
// MARK: - Numeric Conversion Tests
|
|
|
|
func testInt32FromInt() {
|
|
// Given: Int value
|
|
let intValue = 42
|
|
|
|
// When: Convert to Int32
|
|
let int32Value = DailyNotificationDataConversions.int32FromInt(intValue)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(int32Value, 42, "Int to Int32 conversion should preserve value")
|
|
}
|
|
|
|
func testIntFromInt32() {
|
|
// Given: Int32 value
|
|
let int32Value: Int32 = 42
|
|
|
|
// When: Convert to Int
|
|
let intValue = DailyNotificationDataConversions.intFromInt32(int32Value)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(intValue, 42, "Int32 to Int conversion should preserve value")
|
|
}
|
|
|
|
func testInt32FromInt64_WithinRange() {
|
|
// Given: Int64 value within Int32 range
|
|
let int64Value: Int64 = 2147483647 // Int32.max
|
|
|
|
// When: Convert to Int32
|
|
let int32Value = DailyNotificationDataConversions.int32FromInt64(int64Value)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(int32Value, Int32.max, "Int64 to Int32 conversion should work within range")
|
|
}
|
|
|
|
func testInt32FromInt64_AboveMax() {
|
|
// Given: Int64 value above Int32.max
|
|
let int64Value: Int64 = 2147483648 // Int32.max + 1
|
|
|
|
// When: Convert to Int32
|
|
let int32Value = DailyNotificationDataConversions.int32FromInt64(int64Value)
|
|
|
|
// Then: Should be clamped to Int32.max
|
|
XCTAssertEqual(int32Value, Int32.max, "Int64 above max should be clamped")
|
|
}
|
|
|
|
func testInt32FromInt64_BelowMin() {
|
|
// Given: Int64 value below Int32.min
|
|
let int64Value: Int64 = -2147483649 // Int32.min - 1
|
|
|
|
// When: Convert to Int32
|
|
let int32Value = DailyNotificationDataConversions.int32FromInt64(int64Value)
|
|
|
|
// Then: Should be clamped to Int32.min
|
|
XCTAssertEqual(int32Value, Int32.min, "Int64 below min should be clamped")
|
|
}
|
|
|
|
func testInt64FromInt32() {
|
|
// Given: Int32 value
|
|
let int32Value: Int32 = 42
|
|
|
|
// When: Convert to Int64
|
|
let int64Value = DailyNotificationDataConversions.int64FromInt32(int32Value)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(int64Value, 42, "Int32 to Int64 conversion should preserve value")
|
|
}
|
|
|
|
func testInt64FromLong() {
|
|
// Given: Int64 value (Long)
|
|
let longValue: Int64 = 42
|
|
|
|
// When: Convert to Int64 (no-op)
|
|
let int64Value = DailyNotificationDataConversions.int64FromLong(longValue)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(int64Value, 42, "Long to Int64 conversion should preserve value")
|
|
}
|
|
|
|
func testBoolFromBoolean() {
|
|
// Given: Bool value
|
|
let boolValue = true
|
|
|
|
// When: Convert to Bool (no-op)
|
|
let convertedBool = DailyNotificationDataConversions.boolFromBoolean(boolValue)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(convertedBool, true, "Boolean to Bool conversion should preserve value")
|
|
}
|
|
|
|
// MARK: - String Conversion Tests
|
|
|
|
func testStringFromOptional_Nil() {
|
|
// Given: Nil optional String
|
|
let optionalString: String? = nil
|
|
|
|
// When: Convert to String
|
|
let string = DailyNotificationDataConversions.stringFromOptional(optionalString)
|
|
|
|
// Then: Should be empty string
|
|
XCTAssertEqual(string, "", "Nil optional should produce empty string")
|
|
}
|
|
|
|
func testStringFromOptional_Value() {
|
|
// Given: Optional String with value
|
|
let optionalString: String? = "test"
|
|
|
|
// When: Convert to String
|
|
let string = DailyNotificationDataConversions.stringFromOptional(optionalString)
|
|
|
|
// Then: Should match
|
|
XCTAssertEqual(string, "test", "Non-nil optional should produce value")
|
|
}
|
|
|
|
func testOptionalStringFromString_Empty() {
|
|
// Given: Empty String
|
|
let string = ""
|
|
|
|
// When: Convert to optional String
|
|
let optionalString = DailyNotificationDataConversions.optionalStringFromString(string)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(optionalString, "Empty string should produce nil")
|
|
}
|
|
|
|
func testOptionalStringFromString_Value() {
|
|
// Given: Non-empty String
|
|
let string = "test"
|
|
|
|
// When: Convert to optional String
|
|
let optionalString = DailyNotificationDataConversions.optionalStringFromString(string)
|
|
|
|
// Then: Should have value
|
|
XCTAssertEqual(optionalString, "test", "Non-empty string should produce value")
|
|
}
|
|
|
|
// MARK: - JSON Conversion Tests
|
|
|
|
func testJsonStringFromDictionary_Valid() {
|
|
// Given: Valid dictionary
|
|
let dict: [String: Any] = ["key1": "value1", "key2": 42]
|
|
|
|
// When: Convert to JSON string
|
|
let jsonString = DailyNotificationDataConversions.jsonStringFromDictionary(dict)
|
|
|
|
// Then: Should be valid JSON
|
|
XCTAssertNotNil(jsonString, "Valid dictionary should produce JSON string")
|
|
|
|
// Verify can be parsed back
|
|
let parsedDict = DailyNotificationDataConversions.dictionaryFromJsonString(jsonString)
|
|
XCTAssertNotNil(parsedDict, "JSON string should be parseable")
|
|
XCTAssertEqual(parsedDict?["key1"] as? String, "value1")
|
|
XCTAssertEqual(parsedDict?["key2"] as? Int, 42)
|
|
}
|
|
|
|
func testJsonStringFromDictionary_Nil() {
|
|
// Given: Nil dictionary
|
|
let dict: [String: Any]? = nil
|
|
|
|
// When: Convert to JSON string
|
|
let jsonString = DailyNotificationDataConversions.jsonStringFromDictionary(dict)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(jsonString, "Nil dictionary should produce nil")
|
|
}
|
|
|
|
func testDictionaryFromJsonString_Valid() {
|
|
// Given: Valid JSON string
|
|
let jsonString = "{\"key1\":\"value1\",\"key2\":42}"
|
|
|
|
// When: Convert to dictionary
|
|
let dict = DailyNotificationDataConversions.dictionaryFromJsonString(jsonString)
|
|
|
|
// Then: Should be valid dictionary
|
|
XCTAssertNotNil(dict, "Valid JSON string should produce dictionary")
|
|
XCTAssertEqual(dict?["key1"] as? String, "value1")
|
|
XCTAssertEqual(dict?["key2"] as? Int, 42)
|
|
}
|
|
|
|
func testDictionaryFromJsonString_Invalid() {
|
|
// Given: Invalid JSON string
|
|
let jsonString = "{invalid json}"
|
|
|
|
// When: Convert to dictionary
|
|
let dict = DailyNotificationDataConversions.dictionaryFromJsonString(jsonString)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(dict, "Invalid JSON string should produce nil")
|
|
}
|
|
|
|
func testDictionaryFromJsonString_Nil() {
|
|
// Given: Nil JSON string
|
|
let jsonString: String? = nil
|
|
|
|
// When: Convert to dictionary
|
|
let dict = DailyNotificationDataConversions.dictionaryFromJsonString(jsonString)
|
|
|
|
// Then: Should be nil
|
|
XCTAssertNil(dict, "Nil JSON string should produce nil")
|
|
}
|
|
|
|
func testJsonStringFromDictionary_RoundTrip() {
|
|
// Given: Original dictionary
|
|
let originalDict: [String: Any] = [
|
|
"string": "value",
|
|
"number": 42,
|
|
"bool": true,
|
|
"nested": ["key": "value"]
|
|
]
|
|
|
|
// When: Convert to JSON and back
|
|
let jsonString = DailyNotificationDataConversions.jsonStringFromDictionary(originalDict)
|
|
let parsedDict = DailyNotificationDataConversions.dictionaryFromJsonString(jsonString)
|
|
|
|
// Then: Should match original (with type conversions)
|
|
XCTAssertNotNil(parsedDict, "Round trip should produce dictionary")
|
|
XCTAssertEqual(parsedDict?["string"] as? String, "value")
|
|
XCTAssertEqual(parsedDict?["number"] as? Int, 42)
|
|
XCTAssertEqual(parsedDict?["bool"] as? Bool, true)
|
|
}
|
|
}
|
|
|