Files
daily-notification-plugin/ios/Tests/DailyNotificationDataConversionsTests.swift
Matthew a90d08c425 feat(ios): add Core Data DAO layer and unit tests
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.
2025-12-09 02:23:05 -08:00

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)
}
}