Files
iOS/Tests/Shared/Database/GRDB+Initialization.test.swift
Bruno Pantaleão Gonçalves 89bad79a3d Remove left over from unreleased feature camera list native UI (#4717)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
2026-06-09 13:07:43 +00:00

235 lines
8.7 KiB
Swift

import GRDB
@testable import Shared
import Testing
@Suite("GRDB Initialization Tests")
struct GRDBInitializationTests {
/// Helper to create a unique test database path
func makeTestDatabasePath() -> String {
let tempDirectory = NSTemporaryDirectory()
return (tempDirectory as NSString).appendingPathComponent("test_grdb_\(UUID().uuidString).sqlite")
}
/// Helper to clean up test database
func cleanupDatabase(at path: String) {
try? FileManager.default.removeItem(atPath: path)
}
@Test("Database path in test environment")
func databasePathInTestEnvironment() throws {
// The test environment variable should already be set by the test framework
let path = DatabaseQueue.databasePath()
// In test environment, path should be in temp directory
#expect(
path.contains(NSTemporaryDirectory()) || path.contains("test_database.sqlite"),
"Database path in test environment should use temp directory or test_database.sqlite"
)
}
@Test("Tables returns exactly 16 tables")
func tablesReturns16Tables() throws {
let tables = DatabaseQueue.tables()
#expect(tables.count == 16, "DatabaseQueue.tables() should return exactly 16 tables")
}
@Test("Tables contains all expected table names")
func tablesContainsAllExpectedTables() throws {
let tables = DatabaseQueue.tables()
let tableNames = tables.map(\.tableName)
// Verify all expected table names are present
let expectedTableNames = [
GRDBDatabaseTable.HAAppEntity.rawValue,
GRDBDatabaseTable.watchConfig.rawValue,
GRDBDatabaseTable.carPlayConfig.rawValue,
GRDBDatabaseTable.appIconShortcutConfig.rawValue,
GRDBDatabaseTable.assistPipelines.rawValue,
GRDBDatabaseTable.serverInfoMirror.rawValue,
GRDBDatabaseTable.appEntityRegistryListForDisplay.rawValue,
GRDBDatabaseTable.entityRegistry.rawValue,
GRDBDatabaseTable.deviceRegistry.rawValue,
GRDBDatabaseTable.appPanel.rawValue,
GRDBDatabaseTable.customWidget.rawValue,
GRDBDatabaseTable.appArea.rawValue,
GRDBDatabaseTable.homeViewConfiguration.rawValue,
GRDBDatabaseTable.assistConfiguration.rawValue,
GRDBDatabaseTable.kioskSettings.rawValue,
GRDBDatabaseTable.allowedTags.rawValue,
]
for expectedName in expectedTableNames {
#expect(
tableNames.contains(expectedName),
"tables() should contain table named '\(expectedName)'"
)
}
}
@Test("App database creates all tables")
func appDatabaseCreatesAllTables() throws {
// Create a test database using the static method
let testDatabasePath = makeTestDatabasePath()
defer { cleanupDatabase(at: testDatabasePath) }
let database = try DatabaseQueue(path: testDatabasePath)
// Create all tables
for table in DatabaseQueue.tables() {
try table.createIfNeeded(database: database)
}
// Verify all tables exist
let existingTables = try database.read { db in
try String.fetchAll(db, sql: "SELECT name FROM sqlite_master WHERE type='table'")
}
for table in DatabaseQueue.tables() {
#expect(
existingTables.contains(table.tableName),
"Database should contain table '\(table.tableName)'"
)
}
}
@Test("deleteOldTables removes clientEvent table")
func deleteOldTablesRemovesClientEventTable() throws {
let testDatabasePath = makeTestDatabasePath()
defer { cleanupDatabase(at: testDatabasePath) }
let database = try DatabaseQueue(path: testDatabasePath)
// Create the old clientEvent table
try database.write { db in
try db.create(table: GRDBDatabaseTable.clientEvent.rawValue) { t in
t.column("id", .text).primaryKey()
t.column("text", .text)
}
}
// Verify clientEvent table exists
var tableExists = try database.read { db in
try db.tableExists(GRDBDatabaseTable.clientEvent.rawValue)
}
#expect(tableExists, "clientEvent table should exist before cleanup")
// Call deleteOldTables
DatabaseQueue.deleteOldTables(database: database)
// Verify clientEvent table no longer exists
tableExists = try database.read { db in
try db.tableExists(GRDBDatabaseTable.clientEvent.rawValue)
}
#expect(!tableExists, "clientEvent table should not exist after cleanup")
}
@Test("deleteOldTables handles missing table gracefully")
func deleteOldTablesHandlesMissingTableGracefully() throws {
let testDatabasePath = makeTestDatabasePath()
defer { cleanupDatabase(at: testDatabasePath) }
let database = try DatabaseQueue(path: testDatabasePath)
// Verify clientEvent table doesn't exist
let tableExists = try database.read { db in
try db.tableExists(GRDBDatabaseTable.clientEvent.rawValue)
}
#expect(!tableExists, "clientEvent table should not exist initially")
// Call deleteOldTables (should not throw error since test is marked as `throws`)
DatabaseQueue.deleteOldTables(database: database)
}
@Test("Table creation error logs to ClientEventStore")
func tableCreationErrorLogsToClientEventStore() throws {
let testDatabasePath = makeTestDatabasePath()
defer { cleanupDatabase(at: testDatabasePath) }
// Mock the client event store
var loggedEvents: [ClientEvent] = []
let mockStore = MockClientEventStore(onAddEvent: { event in
loggedEvents.append(event)
})
// Replace Current.clientEventStore temporarily
let originalStore = Current.clientEventStore
Current.clientEventStore = mockStore
defer { Current.clientEventStore = originalStore }
// Create a test table that will fail during creation
class FailingTable: DatabaseTableProtocol {
var tableName: String { "failingTable" }
var definedColumns: [String] { ["column1"] }
func createIfNeeded(database: DatabaseQueue) throws {
throw NSError(domain: "TestError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Forced error"])
}
}
let database = try DatabaseQueue(path: testDatabasePath)
let failingTable = FailingTable()
// Try to create the failing table (mimic what appDatabase does)
do {
try failingTable.createIfNeeded(database: database)
} catch {
let className = String(describing: type(of: failingTable))
let errorMessage = "Failed create GRDB table \(className), error: \(error.localizedDescription)"
mockStore.addEvent(ClientEvent(text: errorMessage, type: .database))
}
// Verify error was logged
#expect(loggedEvents.count == 1, "Should have logged 1 error")
#expect(
loggedEvents.first?.text.contains("Failed create GRDB table") ?? false,
"Error message should contain 'Failed create GRDB table'"
)
#expect(loggedEvents.first?.type == .database, "Error type should be .database")
}
@Test("Multiple tables can coexist")
func multipleTablesCanCoexist() throws {
let testDatabasePath = makeTestDatabasePath()
defer { cleanupDatabase(at: testDatabasePath) }
let database = try DatabaseQueue(path: testDatabasePath)
// Create multiple tables
let table1 = HAppEntityTable()
let table2 = WatchConfigTable()
let table3 = CarPlayConfigTable()
try table1.createIfNeeded(database: database)
try table2.createIfNeeded(database: database)
try table3.createIfNeeded(database: database)
// Verify all tables exist
let existingTables = try database.read { db in
try String.fetchAll(db, sql: "SELECT name FROM sqlite_master WHERE type='table'")
}
#expect(existingTables.contains(table1.tableName))
#expect(existingTables.contains(table2.tableName))
#expect(existingTables.contains(table3.tableName))
}
}
// MARK: - Mock Client Event Store
final class MockClientEventStore: ClientEventStoreProtocol {
private let onAddEvent: (ClientEvent) -> Void
init(onAddEvent: @escaping (ClientEvent) -> Void) {
self.onAddEvent = onAddEvent
}
func addEvent(_ event: ClientEvent) {
onAddEvent(event)
}
func getEvents() -> [ClientEvent] {
[]
}
func clearAllEvents() {}
}