You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
287 lines
14 KiB
287 lines
14 KiB
import Foundation
|
|
import Testing
|
|
@testable import Music
|
|
|
|
struct SmartPlaylistTests {
|
|
// Creates a SmartPlaylist in memory and verifies its properties.
|
|
@Test func smartPlaylistProperties() throws {
|
|
let sp = SmartPlaylist(
|
|
id: nil,
|
|
name: "Miles Davis",
|
|
searchQuery: "miles davis",
|
|
createdAt: Date(),
|
|
conditions: nil
|
|
)
|
|
#expect(sp.name == "Miles Davis")
|
|
#expect(sp.searchQuery == "miles davis")
|
|
#expect(sp.isSmartPlaylist == true)
|
|
}
|
|
|
|
// Creates a smart playlist in the database and fetches it back.
|
|
@Test func createAndFetchSmartPlaylist() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
let sp = try db.createSmartPlaylist(name: "Jazz Vibes", searchQuery: "jazz")
|
|
|
|
#expect(sp.id != nil)
|
|
#expect(sp.name == "Jazz Vibes")
|
|
#expect(sp.searchQuery == "jazz")
|
|
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all.count == 1)
|
|
#expect(all[0].name == "Jazz Vibes")
|
|
}
|
|
|
|
// Renames a smart playlist and verifies the new name persists.
|
|
@Test func renameSmartPlaylist() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
let sp = try db.createSmartPlaylist(name: "Old Name", searchQuery: "old")
|
|
try db.renameSmartPlaylist(id: sp.id!, name: "New Name")
|
|
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all[0].name == "New Name")
|
|
}
|
|
|
|
// Updates the search query of a smart playlist.
|
|
@Test func updateSmartPlaylistQuery() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
let sp = try db.createSmartPlaylist(name: "Jazz", searchQuery: "jazz")
|
|
try db.updateSmartPlaylistQuery(id: sp.id!, searchQuery: "jazz fusion")
|
|
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all[0].searchQuery == "jazz fusion")
|
|
}
|
|
|
|
// Deletes a smart playlist and verifies it's gone.
|
|
@Test func deleteSmartPlaylist() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
let sp = try db.createSmartPlaylist(name: "To Delete", searchQuery: "delete")
|
|
try db.deleteSmartPlaylist(id: sp.id!)
|
|
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all.isEmpty)
|
|
}
|
|
|
|
// Verifies smart playlist tracks are computed via FTS5 search (not stored).
|
|
@Test func smartPlaylistReturnsDynamicResults() throws {
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", title: "Kind of Blue", artist: "Miles Davis")
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", title: "Hotel California", artist: "Eagles")
|
|
var t3 = Track.fixture(fileURL: "/c.mp3", title: "Bitches Brew", artist: "Miles Davis")
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
try db.insert(&t3)
|
|
|
|
// Smart playlist uses the existing fetchTracks with its searchQuery
|
|
let results = try db.fetchTracks(search: "miles davis", sortColumn: "title", ascending: true)
|
|
#expect(results.count == 2)
|
|
#expect(results[0].title == "Bitches Brew")
|
|
#expect(results[1].title == "Kind of Blue")
|
|
}
|
|
|
|
// Creates a SmartPlaylist fixture with conditions and verifies the conditions
|
|
// field is preserved and the isSmartPlaylist flag is true.
|
|
@Test func smartPlaylistWithConditions() throws {
|
|
let conditions = [SmartPlaylistCondition(field: .artist, op: .equals, value: .string("Miles Davis"))]
|
|
let sp = SmartPlaylist.fixture(conditions: conditions)
|
|
#expect(sp.conditions?.count == 1)
|
|
#expect(sp.conditions?[0].field == .artist)
|
|
#expect(sp.isSmartPlaylist == true)
|
|
}
|
|
|
|
// Encodes and decodes a SmartPlaylistCondition to/from JSON,
|
|
// verifying that all fields survive the round-trip.
|
|
@Test func conditionCodableRoundTrip() throws {
|
|
let condition = SmartPlaylistCondition(
|
|
field: .artist,
|
|
op: .equals,
|
|
value: .string("Miles Davis")
|
|
)
|
|
let data = try JSONEncoder().encode(condition)
|
|
let decoded = try JSONDecoder().decode(SmartPlaylistCondition.self, from: data)
|
|
#expect(decoded.field == .artist)
|
|
#expect(decoded.op == .equals)
|
|
if case .string(let s) = decoded.value {
|
|
#expect(s == "Miles Davis")
|
|
} else {
|
|
Issue.record("Expected string value")
|
|
}
|
|
}
|
|
|
|
// Encodes and decodes an array of conditions with mixed value types.
|
|
@Test func conditionsArrayCodableRoundTrip() throws {
|
|
let conditions: [SmartPlaylistCondition] = [
|
|
SmartPlaylistCondition(field: .artist, op: .startsWith, value: .string("Miles")),
|
|
SmartPlaylistCondition(field: .year, op: .greaterThan, value: .int(1960)),
|
|
SmartPlaylistCondition(field: .dateAdded, op: .lessThan, value: .date(Date(timeIntervalSince1970: 0)))
|
|
]
|
|
let data = try JSONEncoder().encode(conditions)
|
|
let decoded = try JSONDecoder().decode([SmartPlaylistCondition].self, from: data)
|
|
#expect(decoded.count == 3)
|
|
#expect(decoded[0].field == .artist)
|
|
#expect(decoded[1].op == .greaterThan)
|
|
if case .int(let y) = decoded[1].value { #expect(y == 1960) } else { Issue.record("Expected int") }
|
|
if case .date(let d) = decoded[2].value { #expect(d == Date(timeIntervalSince1970: 0)) } else { Issue.record("Expected date") }
|
|
}
|
|
|
|
// Creates an in-memory DB (which runs all migrations including v5) and verifies
|
|
// that existing FTS smart playlists (conditions = nil) still load correctly.
|
|
@Test func existingFTSPlaylistSurvivesMigration() throws {
|
|
// Step 1: Create DB — migration v5 runs automatically, adding the conditions column
|
|
// Step 2: Create a FTS smart playlist using the old searchQuery path
|
|
// Step 3: Fetch it back and verify conditions is nil, searchQuery is intact
|
|
let db = try DatabaseService(inMemory: true)
|
|
_ = try db.createSmartPlaylist(name: "Jazz", searchQuery: "jazz")
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all.count == 1)
|
|
#expect(all[0].searchQuery == "jazz")
|
|
#expect(all[0].conditions == nil)
|
|
}
|
|
|
|
// Inserts two tracks with different artists, fetches with an equals condition
|
|
// on artist, and verifies only the matching track is returned.
|
|
@Test func fetchTracksWithEqualsCondition() throws {
|
|
// Step 1: Insert two tracks with different artists
|
|
// Step 2: Build a condition: artist equals "Miles Davis"
|
|
// Step 3: Fetch tracks with that condition and verify only one returned
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", title: "Kind of Blue", artist: "Miles Davis")
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", title: "Hotel California", artist: "Eagles")
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
let conditions = [SmartPlaylistCondition(field: .artist, op: .equals, value: .string("Miles Davis"))]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 1)
|
|
#expect(results[0].artist == "Miles Davis")
|
|
}
|
|
|
|
// Verifies that string equals matching is case-insensitive.
|
|
@Test func fetchTracksEqualsIsCaseInsensitive() throws {
|
|
// Step 1: Insert a track with mixed-case artist "Miles Davis"
|
|
// Step 2: Fetch using lowercase "miles davis"
|
|
// Step 3: Verify the track is returned
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t = Track.fixture(fileURL: "/a.mp3", artist: "Miles Davis")
|
|
try db.insert(&t)
|
|
let conditions = [SmartPlaylistCondition(field: .artist, op: .equals, value: .string("miles davis"))]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 1)
|
|
}
|
|
|
|
// Verifies that startsWith matches case-insensitively on the leading prefix.
|
|
@Test func fetchTracksWithStartsWithCondition() throws {
|
|
// Step 1: Insert a Miles Davis track and an Eagles track
|
|
// Step 2: Fetch with artist startsWith "miles" (lowercase)
|
|
// Step 3: Verify only Miles Davis is returned
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", artist: "Miles Davis")
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", artist: "Eagles")
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
let conditions = [SmartPlaylistCondition(field: .artist, op: .startsWith, value: .string("miles"))]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 1)
|
|
#expect(results[0].artist == "Miles Davis")
|
|
}
|
|
|
|
// Verifies that greaterThan on an integer field returns only tracks strictly
|
|
// above the threshold value.
|
|
@Test func fetchTracksWithGreaterThanCondition() throws {
|
|
// Step 1: Insert tracks with years 1990, 2010, 2020
|
|
// Step 2: Fetch with year > 2000
|
|
// Step 3: Verify 2010 and 2020 are returned; 1990 is not
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", year: 1990)
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", year: 2010)
|
|
var t3 = Track.fixture(fileURL: "/c.mp3", year: 2020)
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
try db.insert(&t3)
|
|
let conditions = [SmartPlaylistCondition(field: .year, op: .greaterThan, value: .int(2000))]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 2)
|
|
#expect(results.allSatisfy { ($0.year ?? 0) > 2000 })
|
|
}
|
|
|
|
// Verifies that multiple conditions are AND-ed: only tracks matching all
|
|
// conditions are returned.
|
|
@Test func fetchTracksWithMultipleAndConditions() throws {
|
|
// Step 1: Insert three tracks — two Miles Davis (1959, 1970) and one Eagles (1975)
|
|
// Step 2: Fetch with artist = "Miles Davis" AND year > 1960
|
|
// Step 3: Verify only the 1970 Miles Davis track is returned
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", title: "Kind of Blue", artist: "Miles Davis", year: 1959)
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", title: "Bitches Brew", artist: "Miles Davis", year: 1970)
|
|
var t3 = Track.fixture(fileURL: "/c.mp3", title: "Hotel California", artist: "Eagles", year: 1975)
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
try db.insert(&t3)
|
|
let conditions = [
|
|
SmartPlaylistCondition(field: .artist, op: .equals, value: .string("Miles Davis")),
|
|
SmartPlaylistCondition(field: .year, op: .greaterThan, value: .int(1960))
|
|
]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 1)
|
|
#expect(results[0].title == "Bitches Brew")
|
|
}
|
|
|
|
// Creates a conditions-based smart playlist, fetches it back, and verifies the
|
|
// conditions survive the JSON round-trip through GRDB's Codable synthesis.
|
|
@Test func createSmartPlaylistWithConditionsPersists() throws {
|
|
// Step 1: Create a conditions-based playlist with artist equals and year > conditions
|
|
// Step 2: Fetch all smart playlists
|
|
// Step 3: Verify both conditions survived the DB round-trip intact
|
|
let db = try DatabaseService(inMemory: true)
|
|
let conditions = [
|
|
SmartPlaylistCondition(field: .artist, op: .equals, value: .string("Miles Davis")),
|
|
SmartPlaylistCondition(field: .year, op: .greaterThan, value: .int(1960))
|
|
]
|
|
_ = try db.createSmartPlaylist(name: "Late Miles", conditions: conditions)
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all.count == 1)
|
|
#expect(all[0].conditions?.count == 2)
|
|
#expect(all[0].conditions?[0].field == .artist)
|
|
#expect(all[0].conditions?[1].op == .greaterThan)
|
|
if case .int(let y) = all[0].conditions?[1].value {
|
|
#expect(y == 1960)
|
|
} else {
|
|
Issue.record("Expected int value")
|
|
}
|
|
}
|
|
|
|
// Verifies that lessThan on an integer field returns only tracks strictly
|
|
// below the threshold value.
|
|
@Test func fetchTracksWithLessThanCondition() throws {
|
|
// Step 1: Insert tracks with years 1990, 2010, 2020
|
|
// Step 2: Fetch with year < 2000
|
|
// Step 3: Verify only the 1990 track is returned
|
|
let db = try DatabaseService(inMemory: true)
|
|
var t1 = Track.fixture(fileURL: "/a.mp3", year: 1990)
|
|
var t2 = Track.fixture(fileURL: "/b.mp3", year: 2010)
|
|
var t3 = Track.fixture(fileURL: "/c.mp3", year: 2020)
|
|
try db.insert(&t1)
|
|
try db.insert(&t2)
|
|
try db.insert(&t3)
|
|
let conditions = [SmartPlaylistCondition(field: .year, op: .lessThan, value: .int(2000))]
|
|
let results = try db.fetchTracks(conditions: conditions, sortColumn: "title", ascending: true)
|
|
#expect(results.count == 1)
|
|
#expect((results[0].year ?? 0) < 2000)
|
|
}
|
|
|
|
// Updates the conditions of a structured smart playlist and verifies the updated
|
|
// conditions are persisted and fetch back correctly.
|
|
@Test func updateSmartPlaylistConditionsPersists() throws {
|
|
// Step 1: Create a playlist with artist = "Eagles"
|
|
// Step 2: Update its conditions to genre startsWith "Jazz"
|
|
// Step 3: Fetch and verify the updated conditions are stored
|
|
let db = try DatabaseService(inMemory: true)
|
|
let sp = try db.createSmartPlaylist(
|
|
name: "Test",
|
|
conditions: [SmartPlaylistCondition(field: .artist, op: .equals, value: .string("Eagles"))]
|
|
)
|
|
let newConditions = [SmartPlaylistCondition(field: .genre, op: .startsWith, value: .string("Jazz"))]
|
|
try db.updateSmartPlaylistConditions(id: sp.id!, conditions: newConditions)
|
|
let all = try db.fetchSmartPlaylists()
|
|
#expect(all[0].conditions?.count == 1)
|
|
#expect(all[0].conditions?[0].field == .genre)
|
|
}
|
|
}
|
|
|