import Testing import Foundation import GRDB @testable import Music /// Tests for the remote-DB transfer integrity guard. The bug under test: connecting to /// a remote host downloaded a database that opened with "database disk image is malformed" /// (SQLITE_CORRUPT) on `SELECT * FROM sqlite_master`. The fix validates the database with /// `PRAGMA quick_check` on both ends (host before serving, client after writing) so a /// malformed image is rejected with a clear error instead of crashing the UI. @MainActor struct RemoteDBIntegrityTests { /// Build a real DatabaseService (DatabasePool/WAL, like the running app) at a fresh /// temp path, populated with `count` tracks, and return (tempDir, dbPath). private func makePopulatedDB(count: Int) throws -> (dir: URL, path: String) { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let path = tempDir.appendingPathComponent("src.sqlite").path let db = try DatabaseService(path: path) for i in 1...count { var t = Track.fixture(fileURL: "/track\(i).mp3", title: "Song number \(i) with several words") try db.insert(&t) } return (tempDir, path) } @Test func backupCopyIsWellFormedAndSurvivesHTTPFraming() throws { // 1. Build a source DB large enough to span many pages, like the real ~5.8MB library. let (tempDir, srcPath) = try makePopulatedDB(count: 2000) defer { try? FileManager.default.removeItem(at: tempDir) } // 2. Produce the served copy exactly as the host does (VACUUM INTO via backup()). let copyPath = tempDir.appendingPathComponent("copy.sqlite").path try DatabaseService(path: srcPath).backup(to: copyPath) // 3. The copy must pass the integrity gate the host now applies before serving. #expect(DatabaseService.isWellFormedDatabase(atPath: copyPath)) // 4. Frame the copy into an HTTP response byte-for-byte like HostServer.sendHTTP, // then parse it back like RemoteClient.handleDBData (split on the first \r\n\r\n). let bodyBytes = try Data(contentsOf: URL(fileURLWithPath: copyPath)) var response = Data("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: \(bodyBytes.count)\r\nConnection: close\r\n\r\n".utf8) response.append(bodyBytes) let separator: [UInt8] = [0x0D, 0x0A, 0x0D, 0x0A] let sepRange = try #require(response.range(of: Data(separator))) let parsedBody = Data(response[sepRange.upperBound...]) // 5. The framed-then-parsed body must be byte-identical to the original copy. #expect(parsedBody == bodyBytes) // 6. Writing it out yields a DB that passes the gate, opens cleanly, and has all rows. let recvPath = tempDir.appendingPathComponent("received.sqlite").path try parsedBody.write(to: URL(fileURLWithPath: recvPath)) #expect(DatabaseService.isWellFormedDatabase(atPath: recvPath)) let reopened = try DatabaseService(path: recvPath) let n = try reopened.dbPool.read { db in try Int.fetchOne(db, sql: "SELECT count(*) FROM tracks") } #expect(n == 2000) } @Test func truncatedDownloadIsRejected() throws { // Reproduces the user's exact symptom: a page-aligned but incomplete SQLite image. // 1. Build a valid served copy. let (tempDir, srcPath) = try makePopulatedDB(count: 2000) defer { try? FileManager.default.removeItem(at: tempDir) } let copyPath = tempDir.appendingPathComponent("copy.sqlite").path try DatabaseService(path: srcPath).backup(to: copyPath) let full = try Data(contentsOf: URL(fileURLWithPath: copyPath)) // 2. Keep the first half — still a whole number of 4096-byte pages with a valid // SQLite header, exactly the shape of the user's "5828608 bytes" malformed file. let truncatedLen = (full.count / 2 / 4096) * 4096 let truncated = Data(full.prefix(truncatedLen)) let truncPath = tempDir.appendingPathComponent("truncated.sqlite").path try truncated.write(to: URL(fileURLWithPath: truncPath)) // 3. The gate must reject it. Before the fix the client handed this straight to // GRDB, which crashed with "database disk image is malformed". #expect(DatabaseService.isWellFormedDatabase(atPath: truncPath) == false) } @Test func corruptInteriorBytesAreRejected() throws { // 1. Build a valid served copy. let (tempDir, srcPath) = try makePopulatedDB(count: 2000) defer { try? FileManager.default.removeItem(at: tempDir) } let copyPath = tempDir.appendingPathComponent("copy.sqlite").path try DatabaseService(path: srcPath).backup(to: copyPath) var bytes = try Data(contentsOf: URL(fileURLWithPath: copyPath)) // 2. Leave the SQLite header intact (so it's still recognized as a database — this // yields SQLITE_CORRUPT/error 11, like the user saw, not "not a database"/error 26) // but smash a stretch of an interior b-tree page. for i in 4096..<4600 { bytes[i] = 0xFF } let corruptPath = tempDir.appendingPathComponent("corrupt.sqlite").path try bytes.write(to: URL(fileURLWithPath: corruptPath)) // 3. The gate must reject the malformed image. #expect(DatabaseService.isWellFormedDatabase(atPath: corruptPath) == false) } @Test func emptyMissingAndGarbageFilesAreRejected() throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } // 1. A missing file is not well-formed. #expect(DatabaseService.isWellFormedDatabase(atPath: tempDir.appendingPathComponent("nope.sqlite").path) == false) // 2. An empty file (0 bytes) is not well-formed. let emptyPath = tempDir.appendingPathComponent("empty.sqlite").path try Data().write(to: URL(fileURLWithPath: emptyPath)) #expect(DatabaseService.isWellFormedDatabase(atPath: emptyPath) == false) // 3. A short text body (e.g. an HTTP error message written as if it were a DB) is rejected. let garbagePath = tempDir.appendingPathComponent("garbage.sqlite").path try Data("Failed to read database".utf8).write(to: URL(fileURLWithPath: garbagePath)) #expect(DatabaseService.isWellFormedDatabase(atPath: garbagePath) == false) } }