luminate/Tests/LuminateStoreTests/SchemaMigratorTests.swift

157 lines
6.1 KiB
Swift

//
// SchemaMigratorTests.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
import Testing
@testable import LuminateCore
@testable import LuminateStore
@Suite struct SchemaMigratorTests {
@Test("Migrating a current database is a no-op")
func currentDatabase() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
try SchemaMigrator.migrate(database)
let snapshot = try SQLitePreferenceStore(url: url).initialSnapshot
try SchemaMigrator.migrate(database)
#expect(try database.userVersion == SchemaMigrator.latestVersion)
#expect(try SQLitePreferenceStore(url: url).initialSnapshot == snapshot)
}
@Test("A newer minimum reader version rejects the database")
func newerReaderVersion() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
try SchemaMigrator.migrate(database)
try database.exec(
"UPDATE schema_metadata SET minimum_reader_version = \(SchemaMigrator.latestVersion + 1);"
)
do {
_ = try SQLitePreferenceStore(url: url)
Issue.record("Expected a newer-reader migration error")
} catch let error as PreferenceStoreError {
#expect(
error
== .requiresNewerApplication(
minimumReaderVersion: SchemaMigrator.latestVersion + 1,
supportedVersion: SchemaMigrator.latestVersion
)
)
}
}
@Test("A non-Luminate application identifier is rejected")
func foreignApplication() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
try database.setApplicationID(1234)
do {
try SchemaMigrator.migrate(database)
Issue.record("Expected a foreign-database error")
} catch let error as PreferenceStoreError {
#expect(error == .notALuminateDatabase(applicationID: 1234))
}
}
@Test("A higher compatible schema version remains readable")
func compatibleHigherVersion() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
try SchemaMigrator.migrate(database)
try database.setUserVersion(SchemaMigrator.latestVersion + 1)
let statement = try SQLiteStatement(
database: database,
sql: "INSERT INTO preference (key, value) VALUES (?1, ?2);"
)
try statement.bind(1, text: "server.url")
try statement.bind(2, .text("http://compatible"))
_ = try statement.step()
let store = try SQLitePreferenceStore(url: url)
#expect(store.initialSnapshot["server.url"] == .text("http://compatible"))
}
@Test("A failed migration rolls back its statements and version")
func failedMigrationRollsBack() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
let failingMigration = StoreMigration(
version: 2,
name: "failing migration",
minimumReaderVersion: 1,
statements: [
"CREATE TABLE version_two_marker (id INTEGER);",
"CREATE TABLE preference (x);",
]
)
#expect(throws: PreferenceStoreError.self) {
try SchemaMigrator.migrate(database, migrations: SchemaMigrator.migrations + [failingMigration])
}
#expect(try database.userVersion == 1)
#expect(try tableExists("version_two_marker", in: database) == false)
}
@Test("Migration versions must be strictly increasing")
func rejectsMalformedMigrationOrder() throws {
let (url, directory) = makeDatabaseURL()
defer { try? FileManager.default.removeItem(at: directory) }
let database = try SQLiteDatabase(url: url)
let migrations = [
StoreMigration(version: 2, name: "second", minimumReaderVersion: 1, statements: []),
StoreMigration(version: 1, name: "first", minimumReaderVersion: 1, statements: []),
]
#expect(throws: PreferenceStoreError.malformedMigrationList(version: 1)) {
try SchemaMigrator.migrate(database, migrations: migrations)
}
}
/// Checks whether SQLite exposes a table with the requested name.
private func tableExists(_ name: String, in database: SQLiteDatabase) throws -> Bool {
let statement = try SQLiteStatement(
database: database,
sql: "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?1;"
)
try statement.bind(1, text: name)
return try statement.step()
}
private func makeDatabaseURL() -> (URL, URL) {
let directory = FileManager.default.temporaryDirectory
.appending(path: "luminate-schema-\(UUID().uuidString)", directoryHint: .isDirectory)
return (directory.appending(path: "preferences.sqlite"), directory)
}
}