157 lines
6.1 KiB
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)
|
|
}
|
|
}
|