feat(docs): complete P2.6 type safety cleanup and P2.7 system invariants
P2.6: Type Safety Cleanup - Replaced 'any' return types in vite-plugin.ts with concrete types (UserConfig, transform return type) - Documented TypeScript mixin 'any[]' exception in PlatformServiceMixin.ts - Audit confirmed: zero 'any' in codebase except documented TS mixin limitation - All external boundaries use 'unknown', all data payloads use 'Record<string, unknown>' P2.7: System Invariants Documentation - Created SYSTEM_INVARIANTS.md documenting all 6 enforced invariants - Added to docs/00-INDEX.md under Policy & Contracts section - Each invariant includes: What, Why, How, Where Progress Docs Updates: - Updated 00-STATUS.md: marked P2.6/P2.7 complete, added type safety invariant note - Updated 01-CHANGELOG-WORK.md: added 2025-12-22 entries for P2.6/P2.7 - Updated 03-TEST-RUNS.md: added P2.6 type safety audit test run - Updated P2-DESIGN.md: marked P2.6 acceptance criteria complete - Updated SYSTEM_INVARIANTS.md: added Type Safety Notes section Baseline Tag: - Created v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete TypeScript compilation: ✅ PASSES Build: ✅ PASSES CI: ✅ All checks pass
This commit is contained in:
115
ios/Tests/TestDBFactory.swift
Normal file
115
ios/Tests/TestDBFactory.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// TestDBFactory.swift
|
||||
// DailyNotificationPluginTests
|
||||
//
|
||||
// Created by Matthew Raymer on 2025-12-16
|
||||
// Copyright © 2025 TimeSafari. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
@testable import DailyNotificationPlugin
|
||||
|
||||
/**
|
||||
* Test database factory for recovery testing
|
||||
*
|
||||
* Provides utilities to create test databases with intentionally invalid/corrupt data
|
||||
* for testing recovery scenarios.
|
||||
*/
|
||||
class TestDBFactory {
|
||||
|
||||
/**
|
||||
* Create a clean test database
|
||||
*
|
||||
* @return Tuple of (database, path)
|
||||
*/
|
||||
static func createCleanDatabase() -> (DailyNotificationDatabase, String) {
|
||||
let testDbPath = NSTemporaryDirectory().appending("test_recovery_db_\(UUID().uuidString).sqlite")
|
||||
let database = DailyNotificationDatabase(path: testDbPath)
|
||||
return (database, testDbPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject invalid notification record directly into database
|
||||
*
|
||||
* @param database Database instance
|
||||
* @param id Notification ID (can be empty for invalid test)
|
||||
* @param scheduledTime Scheduled time (can be invalid/negative)
|
||||
* @param payloadJSON Payload (can be invalid JSON)
|
||||
*/
|
||||
static func injectInvalidNotificationRecord(
|
||||
database: DailyNotificationDatabase,
|
||||
id: String = "",
|
||||
scheduledTime: Int64 = -1,
|
||||
payloadJSON: String = "invalid json {"
|
||||
) {
|
||||
// Direct SQL injection for testing (using executeSQL which is public)
|
||||
let escapedId = id.replacingOccurrences(of: "'", with: "''")
|
||||
let escapedPayload = payloadJSON.replacingOccurrences(of: "'", with: "''")
|
||||
let sql = """
|
||||
INSERT INTO \(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS) (
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID),
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON),
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT),
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_ETAG)
|
||||
) VALUES ('\(escapedId)', '\(escapedPayload)', \(scheduledTime), NULL);
|
||||
"""
|
||||
database.executeSQL(sql)
|
||||
print("TestDBFactory: Injected invalid notification record: id=\(id), time=\(scheduledTime)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject notification with null/empty required fields
|
||||
*/
|
||||
static func injectNotificationWithNullFields(database: DailyNotificationDatabase) {
|
||||
let sql = """
|
||||
INSERT INTO \(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS) (
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID),
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON),
|
||||
\(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT)
|
||||
) VALUES (NULL, '', 0);
|
||||
"""
|
||||
database.executeSQL(sql)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject duplicate notification records (same ID, different times)
|
||||
*/
|
||||
static func injectDuplicateNotifications(
|
||||
database: DailyNotificationDatabase,
|
||||
id: String,
|
||||
times: [Int64]
|
||||
) {
|
||||
for time in times {
|
||||
injectInvalidNotificationRecord(
|
||||
database: database,
|
||||
id: id,
|
||||
scheduledTime: time,
|
||||
payloadJSON: "{\"title\":\"Test\",\"body\":\"Body\"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset database (drop and recreate tables)
|
||||
*/
|
||||
static func resetDatabase(database: DailyNotificationDatabase) {
|
||||
database.executeSQL("DROP TABLE IF EXISTS \(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS);")
|
||||
database.executeSQL("DROP TABLE IF EXISTS \(DailyNotificationDatabase.TABLE_NOTIF_DELIVERIES);")
|
||||
database.executeSQL("DROP TABLE IF EXISTS \(DailyNotificationDatabase.TABLE_NOTIF_CONFIG);")
|
||||
// Recreate tables by opening a new connection
|
||||
let _ = DailyNotificationDatabase(path: database.getPath())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up test database file
|
||||
*/
|
||||
static func cleanupDatabase(path: String) {
|
||||
let fileManager = FileManager.default
|
||||
if fileManager.fileExists(atPath: path) {
|
||||
try? fileManager.removeItem(atPath: path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user